Exposing the lock manager's WaitForLockers() to SQL

Started by Will Mortensenabout 3 years ago29 messages
#1Will Mortensen
will@extrahop.com

Hi there,

We'd like to be able to call the lock manager's WaitForLockers() and
WaitForLockersMultiple() from SQL. Below I describe our use case, but
basically I'm wondering if this:

1. Seems like a reasonable thing to do

2. Would be of interest upstream

3. Should be done with a new pg_foo() function (taking an
oid?), or a whole new SQL command, or something else

If this sounds promising, we may be able to code this up and submit it.

The rest of this email describes our use cases and related observations:

==== Use Case Background ====

Our use case is inspired by this blog post by Marco Slot (CC'ed) at
Citus Data: https://www.citusdata.com/blog/2018/06/14/scalable-incremental-data-aggregation/
. This describes a scheme for correctly aggregating rows given minimal
coordination with an arbitrary number of writers while keeping minimal
additional state. It relies on two simple facts:

1. INSERT/UPDATE take their ROW EXCLUSIVE lock on the target
table before evaluating any column DEFAULT expressions,
and thus before e.g. calling nextval() on a sequence in
the DEFAULT expression. And of course, this lock is only
released when the transaction commits or rolls back.

2. pg_sequence_last_value() (still undocumented!) can be
used to obtain an instantaneous upper bound on the
sequence values that have been returned by nextval(), even
if the transaction that called nextval() hasn't yet
committed.

So, assume we have a table:

create table tbl (
id bigserial,
data text
);

which is only ever modified by INSERTs that use DEFAULT for id. Then,
a client can process each row exactly once using a loop like this
(excuse the pseudo-SQL):

min_id := 0;
while true:
max_id := pg_sequence_last_value('tbl_id_seq');
wait_for_writers('tbl'::regclass);
SELECT
some_aggregation(data)
FROM tbl
WHERE id > min_id AND id <= max_id;
min_id := max_id;

In the blog post, the equivalent of wait_for_writers() is implemented
by taking and immediately releasing a SHARE ROW EXCLUSIVE lock on tbl.
It's unclear why this can't be SHARE, since it just needs to conflict
with INSERT's ROW EXCLUSIVE, but in any case it's sufficient for
correctness.

(Note that this version only works if the rows committed by the
transactions that it waited for are actually visible to the SELECT, so
for example, the whole thing can't be within a Repeatable Read or
Serializable transaction.)

==== Why WaitForLockers()? ====

No new writer can acquire a ROW EXCLUSIVE lock as long as we're
waiting to obtain the SHARE lock, even if we only hold it for an
instant. If we have to wait a long time, because some existing writer
holds its ROW EXCLUSIVE lock for a long time, this could noticeably
reduce overall writer throughput.

But we don't actually need to obtain a lock at all--and waiting for
transactions that already hold conflicting locks is exactly what
WaitForLockers() / WaitForLockersMultiple() does. Using it instead
would prevent any interference with writers.

==== Appendix: Extensions and Observations ====

Aside from downgrading to SHARE mode and merely waiting instead of
locking, we propose a couple other extensions and observations related
to Citus' scheme. These only tangentially motivate our need for
WaitForLockers(), so you may stop reading here unless the overall
scheme is of interest.

== Separate client for reading sequences and waiting ==

First, in our use case each batch of rows might require extensive
processing as part of a larger operation that doesn't want to block
waiting for writers to commit. A simple extension is to separate the
processing from the determination of sequence values. In other words,
have a single client that sits in a loop:

while true:
seq_val := pg_sequence_last_value('tbl_id_seq');
WaitForLockers('tbl'::regclass, 'SHARE');
publish(seq_val);

and any number of other clients that use the series of published
sequence values to do their own independent processing (maintaining
their own additional state).

This can be extended to multiple tables with WaitForLockersMultiple():

while true:
seq_val1 := pg_sequence_last_value('tbl1_id_seq');
seq_val2 := pg_sequence_last_value('tbl2_id_seq');
WaitForLockersMultiple(
ARRAY['tbl1', 'tbl2']::regclass[], 'SHARE');
publish('tbl1', seq_val1);
publish('tbl2', seq_val2);

Which is clearly more efficient than locking or waiting for the tables
in sequence, hence the desire for that function as well.

== Latency ==

This brings us to a series of observations about latency. If some
writers take a long time to commit, some already-committed rows might
not be processed for a long time. To avoid exacerbating this when
using WaitForLockersMultiple(), which obviously has to wait for the
last writer of any specified table, it should be used with groups of
tables that are generally written by the same transactions.

Also, while in Citus' example the aggregation needs to process each
row exactly once, latency can be reduced if a row may be processed
more than once and if rows can be processed out of order by sequence
value (id), by simply removing the "id <= max_id" term from the WHERE
clause in the reader. This particularly reduces latency if waiting and
processing are separated as described in the above section.

== Updates and latency ==

In our application we have some use cases with a table like:

create table tbl (
id bigint primary key,
data text,
mod_idx bigserial
);

where writers do:

INSERT INTO tbl (id, data) VALUES (1, 'foo')
ON CONFLICT (id) DO UPDATE
SET data = excluded.data, mod_idx = DEFAULT;

and where the reader's job is to continuously replicate rows within a
fixed range of id's in an eventually-consistent fashion. Since the
writer always bumps mod_idx by setting it to DEFAULT, superficially it
seems we can use this scheme with mod_idx:

min_mod_idx := 0;
while true:
max_mod_idx := pg_sequence_last_value('tbl_mod_idx_seq');
WaitForLockers('tbl'::regclass, 'SHARE');
SELECT
do_replicate(id, data, mod_idx)
FROM tbl
WHERE
id >= my_min_id -- app-specific
AND id < my_max_id -- app-specific
AND mod_idx > min_mod_idx
AND mod_idx <= max_mod_idx
ORDER BY mod_idx;
min_mod_idx := max_mod_idx;

This version replicates all rows eventually (if writers stop),
replicates each version of a row at most once (allowing updates to be
skipped if obviated by a later committed update), and replicates
changes in order by mod_idx, which may make bookkeeping easier. But
continuous overlapping updates could keep some rows *perpetually* out
of the reader's reach, leading to *unbounded* latency. If the
application can instead tolerate potentially replicating the same
version of a row more than once, and replicating changes to different
rows out of order by mod_idx, latency can be minimized by removing
"mod_idx <= max_mod_idx" from the WHERE clause. (The ORDER BY should
likely also be removed, since later batches may contain rows with a
lower mod_idx.) The remainder of the scheme still ensures that all
rows are eventually replicated, and limits redundant replication while
keeping minimal state.

== Latency tradeoff and advantages ==

In conclusion, with this scheme there is a tradeoff between minimizing
latency and avoiding redundant processing, where (depending on the
scenario) the amount of latency or redundant processing is related to
the maximum amount of time that a writer transaction holds a ROW
EXCLUSIVE lock on the table. Therefore, this time should be minimized
wherever possible.

This tradeoff seems to be an inherent consequence of the minimalist
advantages of this scheme:

1. If we use WaitForLockers(), no additional locks are taken,
so there's no impact on concurrency of writers

2. If WaitForLockers() is separated from readers, there's no
impact on concurrency/waiting of readers

3. Can be used to guarantee eventual consistency as desired

4. Keeps O(1) state per table (per reader)--no tracking of
individual writers or individual row updates

5. Requires minimal cooperation from writers (just use DEFAULT
expressions that use nextval())

#2Marco Slot
marco.slot@gmail.com
In reply to: Will Mortensen (#1)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Fri, Dec 23, 2022 at 11:43 AM Will Mortensen <will@extrahop.com> wrote:

We'd like to be able to call the lock manager's WaitForLockers() and
WaitForLockersMultiple() from SQL. Below I describe our use case, but
basically I'm wondering if this:

1. Seems like a reasonable thing to do

2. Would be of interest upstream

3. Should be done with a new pg_foo() function (taking an
oid?), or a whole new SQL command, or something else

Definitely +1 on adding a function/syntax to wait for lockers without
actually taking a lock. The get sequence value + lock-and-release
approach is still the only reliable scheme I've found for reliably and
efficiently processing new inserts in PostgreSQL. I'm wondering
whether it could be an option of the LOCK command. (LOCK WAIT ONLY?)

Marco

#3Will Mortensen
will@extrahop.com
In reply to: Marco Slot (#2)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi Marco, thanks for the reply! Glad to know you'd find it useful too. :-)

On Tue, Jan 10, 2023 at 1:01 AM Marco Slot <marco.slot@gmail.com> wrote:

I'm wondering whether it could be an option of the LOCK command.
(LOCK WAIT ONLY?)

I assume that's doable, but just from looking at the docs, it might be
a little confusing. For example, at least if we use
WaitForLockersMultiple(), waiting for multiple tables would happen in
parallel (which I think is good), while locking them is documented to
happen sequentially. Also, normal LOCK is illegal outside a
transaction, but waiting makes perfect sense. (Actually, normal LOCK
makes sense too, if the goal was just to wait. :-) )

By contrast, while LOCK has NOWAIT, and SELECT's locking clause
has NOWAIT and SKIP LOCKED, they only change the blocking/failure
behavior, while success still means taking the lock and has the same
semantics.

But I'm really no expert on SQL syntax or typical practice for things like
this. Anything that works is fine with me. :-)

====

As a possibly superfluous sidebar, I wanted to correct this part of my
original message:

On Fri, Dec 23, 2022 at 11:43 AM Will Mortensen <will@extrahop.com> wrote:

pg_sequence_last_value() (still undocumented!) can be used to
obtain an instantaneous upper bound on the sequence values
that have been returned by nextval(), even if the transaction
that called nextval() hasn't yet committed.

This is true, but not the most important part of making this scheme
work: as you mentioned in the Citus blog post, to avoid missing rows,
we need (and this gives us) an instantaneous *lower* bound on the
sequence values that could be used by transactions that commit after
we finish waiting (and start processing). This doesn't work with
sequence caching, since without somehow inspecting all sessions'
sequence caches, rows with arbitrarily old/low cached sequence
values could be committed arbitrarily far into the future, and we'd
fail to process them.

As you also implied in the blog post, the upper bound is what
allows us to also process each row *exactly* once (instead of at
least once) and in sequence order, if desired.

So those are the respective justifications for both arms of the
WHERE clause: id > min_id AND id <= max_id .

Show quoted text

On Tue, Jan 10, 2023 at 1:01 AM Marco Slot <marco.slot@gmail.com> wrote:

On Fri, Dec 23, 2022 at 11:43 AM Will Mortensen <will@extrahop.com> wrote:

We'd like to be able to call the lock manager's WaitForLockers() and
WaitForLockersMultiple() from SQL. Below I describe our use case, but
basically I'm wondering if this:

1. Seems like a reasonable thing to do

2. Would be of interest upstream

3. Should be done with a new pg_foo() function (taking an
oid?), or a whole new SQL command, or something else

Definitely +1 on adding a function/syntax to wait for lockers without
actually taking a lock. The get sequence value + lock-and-release
approach is still the only reliable scheme I've found for reliably and
efficiently processing new inserts in PostgreSQL. I'm wondering
whether it could be an option of the LOCK command. (LOCK WAIT ONLY?)

Marco

#4Andres Freund
andres@anarazel.de
In reply to: Marco Slot (#2)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi,

On 2023-01-10 10:01:25 +0100, Marco Slot wrote:

On Fri, Dec 23, 2022 at 11:43 AM Will Mortensen <will@extrahop.com> wrote:

We'd like to be able to call the lock manager's WaitForLockers() and
WaitForLockersMultiple() from SQL. Below I describe our use case, but
basically I'm wondering if this:

1. Seems like a reasonable thing to do

2. Would be of interest upstream

3. Should be done with a new pg_foo() function (taking an
oid?), or a whole new SQL command, or something else

Definitely +1 on adding a function/syntax to wait for lockers without
actually taking a lock.

I think such a function would still have to integrate enough with the lock
manager infrastructure to participate in the deadlock detector. Otherwise I
think you'd trivially end up with loads of deadlocks.

Greetings,

Andres Freund

#5Will Mortensen
will@extrahop.com
In reply to: Andres Freund (#4)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi Andres,

On Wed, Jan 11, 2023 at 12:33 PM Andres Freund <andres@anarazel.de> wrote:

I think such a function would still have to integrate enough with the lock
manager infrastructure to participate in the deadlock detector. Otherwise I
think you'd trivially end up with loads of deadlocks.

Could you elaborate on which unusual deadlock concerns arise? To be
clear, WaitForLockers() is an existing function in lmgr.c
(https://github.com/postgres/postgres/blob/216a784829c2c5f03ab0c43e009126cbb819e9b2/src/backend/storage/lmgr/lmgr.c#L986),
and naively it seems like we mostly just need to call it. To my very
limited understanding, from looking at the existing callers and the
implementation of LOCK, that would look something like this
(assuming we're in a SQL command like LOCK and calling unmodified
WaitForLockers() with a single table):

1. Call something like RangeVarGetRelidExtended() with AccessShareLock
to ensure the table is not dropped and obtain the table oid

2. Use SET_LOCKTAG_RELATION() to construct the lock tag from the oid

3. Call WaitForLockers(), which internally calls GetLockConflicts() and
VirtualXactLock(). These certainly take plenty of locks of various types,
and will likely sleep in LockAcquire() waiting for transactions to finish,
but there don't seem to be any unusual pre/postconditions, nor do we
hold any unusual locks already.

Obviously a deadlock is possible if transactions end up waiting for each
other, just as when taking table or row locks, etc., but it seems like this
would be detected as usual?

#6Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#5)
Re: Exposing the lock manager's WaitForLockers() to SQL

I suppose if it's correct that we need to lock the table first (at least
in ACCESS SHARE mode), an option to LOCK perhaps makes
more sense. Maybe you could specify two modes like:

LOCK TABLE IN _lockmode_ MODE AND THEN WAIT FOR CONFLICTS WITH _waitmode_ MODE;

But that might be excessive. :-D And I don't know if there's any
reason to use a _lockmode_ other than ACCESS SHARE.

Show quoted text

On Wed, Jan 11, 2023 at 11:03 PM Will Mortensen <will@extrahop.com> wrote:

Hi Andres,

On Wed, Jan 11, 2023 at 12:33 PM Andres Freund <andres@anarazel.de> wrote:

I think such a function would still have to integrate enough with the lock
manager infrastructure to participate in the deadlock detector. Otherwise I
think you'd trivially end up with loads of deadlocks.

Could you elaborate on which unusual deadlock concerns arise? To be
clear, WaitForLockers() is an existing function in lmgr.c
(https://github.com/postgres/postgres/blob/216a784829c2c5f03ab0c43e009126cbb819e9b2/src/backend/storage/lmgr/lmgr.c#L986),
and naively it seems like we mostly just need to call it. To my very
limited understanding, from looking at the existing callers and the
implementation of LOCK, that would look something like this
(assuming we're in a SQL command like LOCK and calling unmodified
WaitForLockers() with a single table):

1. Call something like RangeVarGetRelidExtended() with AccessShareLock
to ensure the table is not dropped and obtain the table oid

2. Use SET_LOCKTAG_RELATION() to construct the lock tag from the oid

3. Call WaitForLockers(), which internally calls GetLockConflicts() and
VirtualXactLock(). These certainly take plenty of locks of various types,
and will likely sleep in LockAcquire() waiting for transactions to finish,
but there don't seem to be any unusual pre/postconditions, nor do we
hold any unusual locks already.

Obviously a deadlock is possible if transactions end up waiting for each
other, just as when taking table or row locks, etc., but it seems like this
would be detected as usual?

#7Andres Freund
andres@anarazel.de
In reply to: Will Mortensen (#5)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi,

On 2023-01-11 23:03:30 -0800, Will Mortensen wrote:

On Wed, Jan 11, 2023 at 12:33 PM Andres Freund <andres@anarazel.de> wrote:

I think such a function would still have to integrate enough with the lock
manager infrastructure to participate in the deadlock detector. Otherwise I
think you'd trivially end up with loads of deadlocks.

Could you elaborate on which unusual deadlock concerns arise? To be
clear, WaitForLockers() is an existing function in lmgr.c
(https://github.com/postgres/postgres/blob/216a784829c2c5f03ab0c43e009126cbb819e9b2/src/backend/storage/lmgr/lmgr.c#L986),
and naively it seems like we mostly just need to call it.

I know that WaitForLockers() is an existing function :). I'm not sure it's
entirely suitable for your use case. So I mainly wanted to point out that if
you end up writing a separate version of it, you still need to integrate with
the deadlock detection. WaitForLockers() does that by actually acquiring a
lock on the "transaction" its waiting for.

To my very limited understanding, from looking at the existing callers and
the implementation of LOCK, that would look something like this (assuming
we're in a SQL command like LOCK and calling unmodified WaitForLockers()
with a single table):

1. Call something like RangeVarGetRelidExtended() with AccessShareLock
to ensure the table is not dropped and obtain the table oid

2. Use SET_LOCKTAG_RELATION() to construct the lock tag from the oid

3. Call WaitForLockers(), which internally calls GetLockConflicts() and
VirtualXactLock(). These certainly take plenty of locks of various types,
and will likely sleep in LockAcquire() waiting for transactions to finish,
but there don't seem to be any unusual pre/postconditions, nor do we
hold any unusual locks already.

I suspect that keeping the AccessShareLock while doing the WaitForLockers() is
likely to increase the deadlock risk noticeably. I think for the use case you
might get away with resolving the relation names, building the locktags, and
then release the lock, before calling WaitForLockers. If somebody drops the
table or such, you'd presumably still get desired behaviour that way, without
the increased deaadlock risk.

Greetings,

Andres Freund

#8Will Mortensen
will@extrahop.com
In reply to: Andres Freund (#7)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi Andres,

On Thu, Jan 12, 2023 at 11:31 AM Andres Freund <andres@anarazel.de> wrote:

I know that WaitForLockers() is an existing function :). I'm not sure it's
entirely suitable for your use case. So I mainly wanted to point out that if
you end up writing a separate version of it, you still need to integrate with
the deadlock detection.

I see. What about it seems potentially unsuitable?

On 2023-01-11 23:03:30 -0800, Will Mortensen wrote:

To my very limited understanding, from looking at the existing callers and
the implementation of LOCK, that would look something like this (assuming
we're in a SQL command like LOCK and calling unmodified WaitForLockers()
with a single table):

1. Call something like RangeVarGetRelidExtended() with AccessShareLock
to ensure the table is not dropped and obtain the table oid

2. Use SET_LOCKTAG_RELATION() to construct the lock tag from the oid

3. Call WaitForLockers(), which internally calls GetLockConflicts() and
VirtualXactLock(). These certainly take plenty of locks of various types,
and will likely sleep in LockAcquire() waiting for transactions to finish,
but there don't seem to be any unusual pre/postconditions, nor do we
hold any unusual locks already.

I suspect that keeping the AccessShareLock while doing the WaitForLockers() is
likely to increase the deadlock risk noticeably. I think for the use case you
might get away with resolving the relation names, building the locktags, and
then release the lock, before calling WaitForLockers. If somebody drops the
table or such, you'd presumably still get desired behaviour that way, without
the increased deaadlock risk.

That makes sense. I agree it seems fine to just return if e.g. the table is
dropped.

FWIW re: deadlocks in general, I probably didn't highlight it well in my
original email, but the existing solution for this use case (as Marco
described in his blog post) is to actually lock the table momentarily.
Marco's blog post uses ShareRowExclusiveLock, but I think ShareLock is
sufficient for us; in any case, that's stronger than the AccessShareLock that
we need to merely wait.

And actually locking the table with e.g. ShareLock seems perhaps *more*
likely to cause deadlocks (and hurts performance), since it not only waits for
existing conflicting lockers (e.g. RowExclusiveLock) as desired, but also
undesirably blocks other transactions from newly acquiring conflicting locks
in the meantime. Hence the motivation for this feature. :-)

I'm sure I may be missing something though. Thanks for all your feedback. :-)

#9Andres Freund
andres@anarazel.de
In reply to: Will Mortensen (#8)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi,

On 2023-01-12 19:21:00 -0800, Will Mortensen wrote:

FWIW re: deadlocks in general, I probably didn't highlight it well in my
original email, but the existing solution for this use case (as Marco
described in his blog post) is to actually lock the table momentarily.
Marco's blog post uses ShareRowExclusiveLock, but I think ShareLock is
sufficient for us; in any case, that's stronger than the AccessShareLock that
we need to merely wait.

And actually locking the table with e.g. ShareLock seems perhaps *more*
likely to cause deadlocks (and hurts performance), since it not only waits for
existing conflicting lockers (e.g. RowExclusiveLock) as desired, but also
undesirably blocks other transactions from newly acquiring conflicting locks
in the meantime. Hence the motivation for this feature. :-)

I'm sure I may be missing something though. Thanks for all your feedback. :-)

From a deadlock risk pov, it's worse to hold an AccessShareLock and then wait
for other transaction to end, than to just wait for ShareRowExclusiveLock,
without holding any locks.

If you don't hold any locks (*) and wait for a lock, you cannot participate in
a deadlock, because nobody will wait for you. A deadlock is a cycle in the
lock graph, a node can't participate in a deadlock if it doesn't have any
incoming edges, and there can't be incoming edges if there's nothing to wait
on.

Consider a scenario like this:

tx 1: acquires RowExclusiveLock on tbl1 to insert rows
tx 2: acquires AccessShareLock on tbl1
tx 2: WaitForLockers(ShareRowExclusiveLock, tbl1) ends up waiting for tx1
tx 1: truncate tbl1 needs an AccessExclusiveLock

Boom, a simple deadlock. tx1 can't progress, because it can't get
AccessExclusiveLock, and tx2 can't progress because tx1 didn't finish.

But if tx2 directly waited for ShareRowExclusiveLock, there'd not been any
cycle in the lock graph, and everything would have worked.

Regards,

Andres

(*) If you define holding locks expansive, it's impossible to wait for a lock
without holding a lock, since every transaction holds a lock on its own
virtual transactionid. But normally nobody just waits for a transaction that
hasn't done anything.

#10Will Mortensen
will@extrahop.com
In reply to: Andres Freund (#9)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi Andres,

On Thu, Jan 12, 2023 at 7:49 PM Andres Freund <andres@anarazel.de> wrote:

Consider a scenario like this:

tx 1: acquires RowExclusiveLock on tbl1 to insert rows
tx 2: acquires AccessShareLock on tbl1
tx 2: WaitForLockers(ShareRowExclusiveLock, tbl1) ends up waiting for tx1
tx 1: truncate tbl1 needs an AccessExclusiveLock

Oh of course, thanks.

Is it even necessary to take the AccessShareLock? I see that one can call e.g.
RangeVarGetRelidExtended() with NoLock, and from the comments it seems
like that might be OK here?

Did you have any remaining concerns about the suitability of WaitForLockers()
for the use case?

Any thoughts on the syntax? It seems like an option to LOCK (like Marco
suggested) might be simplest to implement albeit a little tricky to document.

Supporting descendant tables looks straightforward enough (just collect more
locktags?). Views look more involved; maybe we can avoid supporting them?

#11Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#10)
1 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Here is a first attempt at a WIP patch. Sorry about the MIME type.

It doesn't take any locks on the tables, but I'm not super confident
that that's safe, so any input would be appreciated.

I omitted view support for simplicity, but if that seems like a
requirement I'll see about adding it. I assume we would need to take
AccessShareLock on views (and release it, per above).

If the syntax and behavior seem roughly correct I'll work on updating the docs.

The commit message at the beginning of the .patch has slightly more commentary.

Thanks for any and all feedback!

Attachments:

0001-Add-WAIT-ONLY-option-to-LOCK.patchapplication/octet-stream; name=0001-Add-WAIT-ONLY-option-to-LOCK.patchDownload
From 6caf0b8d365cef594765901d5220445ccc655425 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Mon, 16 Jan 2023 20:51:26 -0800
Subject: [PATCH] Add WAIT ONLY option to LOCK

Rather than actually taking any locks on the table(s), it simply waits
for conflicting lockers using the existing WaitForLockersMultiple()
function in the lock manager (previously used only by concurrent index
operations). As when actually taking the lock, it doesn't wait for any
conflicting locks acquired after it initially determines the set of
conflicting transactions.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

The syntax allows combining it with NO WAIT, which would perhaps be
useful to simply check for conflicts, but this is not yet implemented.
(NOWAIT + immediately releasing the lock already accomplishes roughly
the same thing.)

Unlike other forms of LOCK, WAIT ONLY is allowed outside a transaction
block, since it makes perfect sense to wait and then e.g. SELECT new
data.

Regardless of the specified locking mode, only SELECT permissions are
required on the table(s).

XXX: docs have not been updated yet.
---
 src/backend/commands/lockcmds.c               | 83 +++++++++++++++++--
 src/backend/parser/gram.y                     | 14 +++-
 src/backend/tcop/utility.c                    | 24 ++++--
 src/include/nodes/parsenodes.h                |  1 +
 src/include/parser/kwlist.h                   |  1 +
 .../isolation/expected/deadlock-wait-only.out | 12 +++
 src/test/isolation/expected/wait-only.out     | 78 +++++++++++++++++
 src/test/isolation/isolation_schedule         |  2 +
 .../isolation/specs/deadlock-wait-only.spec   | 23 +++++
 src/test/isolation/specs/wait-only.spec       | 47 +++++++++++
 src/test/regress/expected/lock.out            | 30 +++++++
 src/test/regress/sql/lock.sql                 | 32 +++++++
 12 files changed, 329 insertions(+), 18 deletions(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-only.out
 create mode 100644 src/test/isolation/expected/wait-only.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-only.spec
 create mode 100644 src/test/isolation/specs/wait-only.spec

diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 43c7d7f4bb..90976abdb1 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -29,7 +30,8 @@
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
-static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait,
+							 List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -43,6 +45,34 @@ void
 LockTableCommand(LockStmt *lockstmt)
 {
 	ListCell   *p;
+	LOCKMODE	lockmode;
+	LOCKMODE   	waitmode;
+	List	   *waitlocktags = NIL;
+	List	  **waitlocktags_p;
+
+	if (lockstmt->waitonly && lockstmt->nowait)
+		/*
+		 * this could be defined to check and error if there are conflicting
+		 * lockers, but it seems unclear if that would be useful, since
+		 * LOCK ... NOWAIT + immediate unlock would do nearly the same thing
+		 */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("NOWAIT is not supported with WAIT ONLY")));
+
+
+	if (lockstmt->waitonly)
+	{
+		lockmode = NoLock;
+		waitmode = lockstmt->mode;
+		waitlocktags_p = &waitlocktags;
+	}
+	else
+	{
+		lockmode = lockstmt->mode;
+		waitmode = NoLock;
+		waitlocktags_p = NULL;
+	}
 
 	/*
 	 * Iterate over the list and process the named relations one at a time
@@ -53,16 +83,37 @@ LockTableCommand(LockStmt *lockstmt)
 		bool		recurse = rv->inh;
 		Oid			reloid;
 
-		reloid = RangeVarGetRelidExtended(rv, lockstmt->mode,
+		reloid = RangeVarGetRelidExtended(rv, lockmode,
 										  lockstmt->nowait ? RVR_NOWAIT : 0,
 										  RangeVarCallbackForLockTable,
-										  (void *) &lockstmt->mode);
+										  (void *) &lockmode);
+		if (waitmode != NoLock)
+		{
+			Oid			dbid;
+			LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+
+			if (IsSharedRelation(reloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+			waitlocktags = lappend(waitlocktags, heaplocktag);
+		}
 
 		if (get_rel_relkind(reloid) == RELKIND_VIEW)
-			LockViewRecurse(reloid, lockstmt->mode, lockstmt->nowait, NIL);
+		{
+			if (lockstmt->waitonly || lockmode == NoLock)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("WAIT ONLY is not supported with views")));
+			LockViewRecurse(reloid, lockmode, lockstmt->nowait, NIL);
+		}
 		else if (recurse)
-			LockTableRecurse(reloid, lockstmt->mode, lockstmt->nowait);
+			LockTableRecurse(reloid, lockmode, lockstmt->nowait,
+							 waitlocktags_p);
 	}
+	if (waitmode != NoLock)
+		WaitForLockersMultiple(waitlocktags, waitmode, false);
 }
 
 /*
@@ -116,7 +167,7 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
  * parent which is enough.
  */
 static void
-LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
+LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait, List **locktags_p)
 {
 	List	   *children;
 	ListCell   *lc;
@@ -126,11 +177,26 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	foreach(lc, children)
 	{
 		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
 
-		/* Parent already locked. */
+		/* Parent already handled. */
 		if (childreloid == reloid)
 			continue;
 
+		if (locktags_p != NULL)
+		{
+			heaplocktag = palloc_object(LOCKTAG);
+			if (IsSharedRelation(childreloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+			*locktags_p = lappend(*locktags_p, heaplocktag);
+		}
+
+		if (lockmode == NoLock)
+			continue;
 		if (!nowait)
 			LockRelationOid(childreloid, lockmode);
 		else if (!ConditionalLockRelationOid(childreloid, lockmode))
@@ -229,7 +295,8 @@ LockViewRecurse_walker(Node *node, LockViewRecurse_context *context)
 				LockViewRecurse(relid, context->lockmode, context->nowait,
 								context->ancestor_views);
 			else if (rte->inh)
-				LockTableRecurse(relid, context->lockmode, context->nowait);
+				LockTableRecurse(relid, context->lockmode, context->nowait,
+								 NULL);
 		}
 
 		return query_tree_walker(query,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..db07eadc68 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -351,7 +351,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_waitonly opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -755,7 +755,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -12117,13 +12117,14 @@ using_clause:
  *
  *****************************************************************************/
 
-LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait
+LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait opt_waitonly
 				{
 					LockStmt   *n = makeNode(LockStmt);
 
 					n->relations = $3;
 					n->mode = $4;
 					n->nowait = $5;
+					n->waitonly = $6;
 					$$ = (Node *) n;
 				}
 		;
@@ -12146,6 +12147,11 @@ opt_nowait:	NOWAIT							{ $$ = true; }
 			| /*EMPTY*/						{ $$ = false; }
 		;
 
+opt_waitonly:
+			WAIT ONLY						{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
 opt_nowait_or_skip:
 			NOWAIT							{ $$ = LockWaitError; }
 			| SKIP LOCKED					{ $$ = LockWaitSkip; }
@@ -17010,6 +17016,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17622,6 +17629,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..49f0a99943 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -354,7 +354,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 				 * restrictions here must match those in
 				 * LockAcquireExtended().
 				 */
-				if (stmt->mode > RowExclusiveLock)
+				if (!stmt->waitonly && stmt->mode > RowExclusiveLock)
 					return COMMAND_OK_IN_READ_ONLY_TXN;
 				else
 					return COMMAND_IS_STRICTLY_READ_ONLY;
@@ -932,13 +932,23 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_LockStmt:
+			{
+				LockStmt *stmt = (LockStmt *) parsetree;
 
-			/*
-			 * Since the lock would just get dropped immediately, LOCK TABLE
-			 * outside a transaction block is presumed to be user error.
-			 */
-			RequireTransactionBlock(isTopLevel, "LOCK TABLE");
-			LockTableCommand((LockStmt *) parsetree);
+				if (!stmt->waitonly)
+				{
+					/*
+					 * Since the lock would just get dropped immediately, and
+					 * simply waiting is better done with WAIT ONLY, LOCK TABLE
+					 * without WAIT ONLY outside a transaction block is presumed
+					 * to be user error.
+					 *
+					 * XXX: the error should clarify that WAIT ONLY is allowed?
+					 */
+					RequireTransactionBlock(isTopLevel, "LOCK TABLE");
+				}
+				LockTableCommand(stmt);
+			}
 			break;
 
 		case T_ConstraintsSetStmt:
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 89335d95e7..a44eaf3aa3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3631,6 +3631,7 @@ typedef struct LockStmt
 	List	   *relations;		/* relations to lock */
 	int			mode;			/* lock mode */
 	bool		nowait;			/* no wait mode */
+	bool		waitonly;		/* wait only mode */
 } LockStmt;
 
 /* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..1cd1ab6dfd 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -461,6 +461,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/isolation/expected/deadlock-wait-only.out b/src/test/isolation/expected/deadlock-wait-only.out
new file mode 100644
index 0000000000..78b4962fa1
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-only.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1re s2as s2swo s1aewo s1c s2c
+step s1re: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2as: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2swo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step s1aewo: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; <waiting ...>
+step s1aewo: <... completed>
+step s2swo: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-only.out b/src/test/isolation/expected/wait-only.out
new file mode 100644
index 0000000000..2358ef653b
--- /dev/null
+++ b/src/test/isolation/expected/wait-only.out
@@ -0,0 +1,78 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1in rlwo w1c rsel rc w2c
+step w1in: INSERT INTO a1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel: SELECT id from a1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+step w2c: COMMIT;
+
+starting permutation: w1in w1c rlwo rsel rc w2c
+step w1in: INSERT INTO a1 VALUES (DEFAULT);
+step w1c: COMMIT;
+step rlwo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY;
+step rsel: SELECT id from a1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+step w2c: COMMIT;
+
+starting permutation: w1in rlwo w2in w2c w1c rsel rc
+step w1in: INSERT INTO a1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2in: INSERT INTO a1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel: SELECT id from a1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in rsv rl w2in w1c rrb w2c rsel rc
+step w1in: INSERT INTO a1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE a1 IN SHARE MODE; <waiting ...>
+step w2in: INSERT INTO a1 VALUES (DEFAULT); <waiting ...>
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step w2in: <... completed>
+step w2c: COMMIT;
+step rsel: SELECT id from a1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in rlwo w2in w1c rsel rc w2c
+step w1in: INSERT INTO a1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2in: INSERT INTO a1 VALUES (DEFAULT);
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel: SELECT id from a1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+step w2c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index c11dc9a420..b83e371aec 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-only
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-only
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-only.spec b/src/test/isolation/specs/deadlock-wait-only.spec
new file mode 100644
index 0000000000..0efca38d60
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-only.spec
@@ -0,0 +1,23 @@
+setup
+{
+  CREATE TABLE a1 ();
+}
+
+teardown
+{
+  DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1re	  { LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1aewo	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; }
+step s1c	  { COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2as		{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2swo	{ LOCK TABLE a1 IN SHARE MODE WAIT ONLY; }
+step s2c		{ COMMIT; }
+
+permutation s1re s2as s2swo s1aewo s1c s2c
diff --git a/src/test/isolation/specs/wait-only.spec b/src/test/isolation/specs/wait-only.spec
new file mode 100644
index 0000000000..f91ac3ba18
--- /dev/null
+++ b/src/test/isolation/specs/wait-only.spec
@@ -0,0 +1,47 @@
+setup
+{
+  CREATE TABLE a1 (id bigserial);
+}
+
+teardown
+{
+  DROP TABLE a1;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1in	{ INSERT INTO a1 VALUES (DEFAULT); }
+step w1c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2in	{ INSERT INTO a1 VALUES (DEFAULT); }
+step w2c	{ COMMIT; }
+
+session reader
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step rsv	{ SAVEPOINT foo; }
+step rl   { LOCK TABLE a1 IN SHARE MODE; }
+step rrb  { ROLLBACK TO foo; }
+step rlwo	{ LOCK TABLE a1 IN SHARE MODE WAIT ONLY; }
+step rsel	{ SELECT id from a1; }
+step rc		{ COMMIT; }
+
+# reader waits for writer1 (writer2 no-op)
+permutation w1in rlwo w1c rsel rc w2c
+
+# no waiting if writer1 committed (writer2 no-op)
+permutation w1in w1c rlwo rsel rc w2c
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in rlwo w2in w2c w1c rsel rc
+# ...while actually taking the lock does block writer2 (even if we release it
+# ASAP)
+permutation w1in rsv rl w2in w1c rrb w2c rsel rc
+
+# reader doesn't wait for lock newly acquired by writer2 while waiting for
+# writer1
+permutation w1in rlwo w2in w1c rsel rc w2c
diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out
index ad137d3645..98de54d68b 100644
--- a/src/test/regress/expected/lock.out
+++ b/src/test/regress/expected/lock.out
@@ -41,6 +41,22 @@ LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+ERROR:  NOWAIT is not supported with WAIT ONLY
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -138,8 +154,22 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ERROR:  permission denied for table lock_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql
index b88488c6d0..df4be3147b 100644
--- a/src/test/regress/sql/lock.sql
+++ b/src/test/regress/sql/lock.sql
@@ -47,6 +47,24 @@ LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
 
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -104,9 +122,23 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
+
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
-- 
2.25.1

#12Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#11)
1 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Updated patch with more tests and a first attempt at doc updates.

As the commit message and doc now point out, using
WaitForLockersMultiple() makes for a behavior difference with actually
locking multiple tables, in that the combined set of conflicting locks
is obtained only once for all tables, rather than obtaining conflicts
and locking / waiting for just the first table and then obtaining
conflicts and locking / waiting for the second table, etc. This is
definitely desirable for my use case, but maybe these kinds of
differences illustrate the potential awkwardness of extending LOCK?

Thanks again for any and all feedback!

Attachments:

v2-0001-Add-WAIT-ONLY-option-to-LOCK-statement.patchapplication/octet-stream; name=v2-0001-Add-WAIT-ONLY-option-to-LOCK-statement.patchDownload
From 27fe294490e8330a503a07f82686e15dca6e51ac Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Mon, 16 Jan 2023 20:51:26 -0800
Subject: [PATCH v2] Add WAIT ONLY option to LOCK statement

Rather than actually taking any locks on the table(s), it simply waits
for conflicting lockers using the existing WaitForLockersMultiple()
function in the lock manager. Unlike when actually taking locks, it
obtains the set of conflicting locks once for all specified tables and
then waits for each of those locks, rather than obtaining conflicts for
the first table and waiting for those before obtaining conflicts for the
second table.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

The syntax allows combining it with NOWAIT, which would perhaps be
useful to simply check for conflicts, but this is not yet implemented.
(NOWAIT + immediately releasing the lock already accomplishes roughly
the same thing.)

Unlike other forms of LOCK, WAIT ONLY is allowed outside a transaction
block, since it makes perfect sense to wait and then e.g. SELECT new
data.

Regardless of the specified locking mode, only SELECT permissions are
required on the table(s).
---
 doc/src/sgml/ref/lock.sgml                    |  52 +++++--
 src/backend/commands/lockcmds.c               |  83 +++++++++--
 src/backend/parser/gram.y                     |  14 +-
 src/backend/tcop/utility.c                    |  24 +++-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 .../isolation/expected/deadlock-wait-only.out |  12 ++
 src/test/isolation/expected/wait-only.out     | 129 ++++++++++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../isolation/specs/deadlock-wait-only.spec   |  23 ++++
 src/test/isolation/specs/wait-only.spec       |  61 +++++++++
 src/test/regress/expected/lock.out            |  40 ++++++
 src/test/regress/sql/lock.sql                 |  41 ++++++
 13 files changed, 456 insertions(+), 27 deletions(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-only.out
 create mode 100644 src/test/isolation/expected/wait-only.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-only.spec
 create mode 100644 src/test/isolation/specs/wait-only.spec

diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index 070855da18..01aec7a741 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT ]
+LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT | WAIT ONLY ]
 
 <phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
 
@@ -42,12 +42,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    error is emitted.  Once obtained, the lock is held for the
    remainder of the current transaction.  (There is no <command>UNLOCK
    TABLE</command> command; locks are always released at transaction
-   end.)
+   end.)  If <literal>WAIT ONLY</literal> is specified, <command>LOCK
+   TABLE</command> simply waits for conflicting locks to be released, and
+   doesn't take any new locks.
   </para>
 
   <para>
    When a view is locked, all relations appearing in the view definition
-   query are also locked recursively with the same lock mode.
+   query are also locked recursively with the same lock mode. Views are not
+   currently supported with <literal>WAIT ONLY</literal>.
   </para>
 
   <para>
@@ -101,6 +104,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    restrictive mode first.
   </para>
 
+  <para>
+   <literal>WAIT ONLY</literal> can be used with <literal>SHARE</literal> mode
+   to wait for already-outstanding writes to be committed or rolled back,
+   without preventing other transactions from newly acquiring conflicting locks
+   or performing writes. This can be useful in conjunction with
+   non-transactional coordination between clients, such as through sequences. As
+   above, clients must be mindful of transaction isolation levels.
+  </para>
+
   <para>
    More information about the lock modes and locking strategies can be
    found in <xref linkend="explicit-locking"/>.
@@ -158,6 +170,22 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>WAIT ONLY</literal></term>
+    <listitem>
+     <para>
+      Specifies that <command>LOCK TABLE</command> should only wait for existing
+      conflicting locks to be released, and not take any new locks. Unlike when
+      actually locking the tables, this first obtains the combined set of
+      conflicting locks for all specified tables, and then waits on all of those
+      locks (rather than obtaining the conflicting locks for the first table and
+      waiting on those, then obtaining the conflicting locks for the second
+      table and waiting on those, and so on). Views are not currently supported
+      with <literal>WAIT ONLY</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
@@ -175,7 +203,9 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
     MODE</literal> (or a less-conflicting mode as described in <xref
     linkend="explicit-locking"/>) is permitted. If a user has
     <literal>SELECT</literal> privileges on the table, <literal>ACCESS SHARE
-    MODE</literal> is permitted.
+    MODE</literal> is permitted. If <literal>WAIT ONLY</literal> is specified,
+    only <literal>SELECT</literal> privileges are required regardless of
+    <replaceable class="parameter">lockmode</replaceable>.
    </para>
 
    <para>
@@ -191,10 +221,13 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    </para>
 
    <para>
-    <command>LOCK TABLE</command> is useless outside a transaction block: the lock
-    would remain held only to the completion of the statement.  Therefore
-    <productname>PostgreSQL</productname> reports an error if <command>LOCK</command>
-    is used outside a transaction block.
+    <command>LOCK TABLE</command> without <literal>WAIT ONLY</literal> is
+    useless outside a transaction block: the lock would remain held only to the
+    completion of the statement, and using <literal>WAIT ONLY</literal> is more
+    efficient if the goal is simply to wait for conflicting locks.  Therefore
+    <productname>PostgreSQL</productname> reports an error if
+    <command>LOCK</command> is used outside a transaction block without
+    <literal>WAIT ONLY</literal>.
     Use
     <link linkend="sql-begin"><command>BEGIN</command></link> and
     <link linkend="sql-commit"><command>COMMIT</command></link>
@@ -262,7 +295,8 @@ COMMIT WORK;
 
   <para>
    Except for <literal>ACCESS SHARE</literal>, <literal>ACCESS EXCLUSIVE</literal>,
-   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes, the
+   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes and the
+   <literal>WAIT ONLY</literal> option, the
    <productname>PostgreSQL</productname> lock modes and the
    <command>LOCK TABLE</command> syntax are compatible with those
    present in <productname>Oracle</productname>.
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 92662cbbc8..a5202dd6d0 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -28,7 +29,8 @@
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
-static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait,
+							 List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -42,6 +44,34 @@ void
 LockTableCommand(LockStmt *lockstmt)
 {
 	ListCell   *p;
+	LOCKMODE	lockmode;
+	LOCKMODE   	waitmode;
+	List	   *waitlocktags = NIL;
+	List	  **waitlocktags_p;
+
+	if (lockstmt->waitonly && lockstmt->nowait)
+		/*
+		 * this could be defined to check and error if there are conflicting
+		 * lockers, but it seems unclear if that would be useful, since
+		 * LOCK ... NOWAIT + immediate unlock would do nearly the same thing
+		 */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("NOWAIT is not supported with WAIT ONLY")));
+
+
+	if (lockstmt->waitonly)
+	{
+		lockmode = NoLock;
+		waitmode = lockstmt->mode;
+		waitlocktags_p = &waitlocktags;
+	}
+	else
+	{
+		lockmode = lockstmt->mode;
+		waitmode = NoLock;
+		waitlocktags_p = NULL;
+	}
 
 	/*
 	 * Iterate over the list and process the named relations one at a time
@@ -52,16 +82,37 @@ LockTableCommand(LockStmt *lockstmt)
 		bool		recurse = rv->inh;
 		Oid			reloid;
 
-		reloid = RangeVarGetRelidExtended(rv, lockstmt->mode,
+		reloid = RangeVarGetRelidExtended(rv, lockmode,
 										  lockstmt->nowait ? RVR_NOWAIT : 0,
 										  RangeVarCallbackForLockTable,
-										  (void *) &lockstmt->mode);
+										  (void *) &lockmode);
+		if (waitmode != NoLock)
+		{
+			Oid			dbid;
+			LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+
+			if (IsSharedRelation(reloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+			waitlocktags = lappend(waitlocktags, heaplocktag);
+		}
 
 		if (get_rel_relkind(reloid) == RELKIND_VIEW)
-			LockViewRecurse(reloid, lockstmt->mode, lockstmt->nowait, NIL);
+		{
+			if (lockstmt->waitonly || lockmode == NoLock)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("WAIT ONLY is not supported with views")));
+			LockViewRecurse(reloid, lockmode, lockstmt->nowait, NIL);
+		}
 		else if (recurse)
-			LockTableRecurse(reloid, lockstmt->mode, lockstmt->nowait);
+			LockTableRecurse(reloid, lockmode, lockstmt->nowait,
+							 waitlocktags_p);
 	}
+	if (waitmode != NoLock)
+		WaitForLockersMultiple(waitlocktags, waitmode, false);
 }
 
 /*
@@ -115,7 +166,7 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
  * parent which is enough.
  */
 static void
-LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
+LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait, List **locktags_p)
 {
 	List	   *children;
 	ListCell   *lc;
@@ -125,11 +176,26 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	foreach(lc, children)
 	{
 		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
 
-		/* Parent already locked. */
+		/* Parent already handled. */
 		if (childreloid == reloid)
 			continue;
 
+		if (locktags_p != NULL)
+		{
+			heaplocktag = palloc_object(LOCKTAG);
+			if (IsSharedRelation(childreloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+			*locktags_p = lappend(*locktags_p, heaplocktag);
+		}
+
+		if (lockmode == NoLock)
+			continue;
 		if (!nowait)
 			LockRelationOid(childreloid, lockmode);
 		else if (!ConditionalLockRelationOid(childreloid, lockmode))
@@ -228,7 +294,8 @@ LockViewRecurse_walker(Node *node, LockViewRecurse_context *context)
 				LockViewRecurse(relid, context->lockmode, context->nowait,
 								context->ancestor_views);
 			else if (rte->inh)
-				LockTableRecurse(relid, context->lockmode, context->nowait);
+				LockTableRecurse(relid, context->lockmode, context->nowait,
+								 NULL);
 		}
 
 		return query_tree_walker(query,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 39ab7eac0d..7314fe0c1e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -353,7 +353,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_waitonly opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -775,7 +775,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -12125,13 +12125,14 @@ using_clause:
  *
  *****************************************************************************/
 
-LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait
+LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait opt_waitonly
 				{
 					LockStmt   *n = makeNode(LockStmt);
 
 					n->relations = $3;
 					n->mode = $4;
 					n->nowait = $5;
+					n->waitonly = $6;
 					$$ = (Node *) n;
 				}
 		;
@@ -12154,6 +12155,11 @@ opt_nowait:	NOWAIT							{ $$ = true; }
 			| /*EMPTY*/						{ $$ = false; }
 		;
 
+opt_waitonly:
+			WAIT ONLY						{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
 opt_nowait_or_skip:
 			NOWAIT							{ $$ = LockWaitError; }
 			| SKIP LOCKED					{ $$ = LockWaitSkip; }
@@ -17241,6 +17247,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17867,6 +17874,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 30b51bf4d3..9491aa248b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -354,7 +354,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 				 * restrictions here must match those in
 				 * LockAcquireExtended().
 				 */
-				if (stmt->mode > RowExclusiveLock)
+				if (!stmt->waitonly && stmt->mode > RowExclusiveLock)
 					return COMMAND_OK_IN_READ_ONLY_TXN;
 				else
 					return COMMAND_IS_STRICTLY_READ_ONLY;
@@ -932,13 +932,23 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_LockStmt:
+			{
+				LockStmt *stmt = (LockStmt *) parsetree;
 
-			/*
-			 * Since the lock would just get dropped immediately, LOCK TABLE
-			 * outside a transaction block is presumed to be user error.
-			 */
-			RequireTransactionBlock(isTopLevel, "LOCK TABLE");
-			LockTableCommand((LockStmt *) parsetree);
+				if (!stmt->waitonly)
+				{
+					/*
+					 * Since the lock would just get dropped immediately, and
+					 * simply waiting is better done with WAIT ONLY, LOCK TABLE
+					 * without WAIT ONLY outside a transaction block is presumed
+					 * to be user error.
+					 *
+					 * XXX: the error should clarify that WAIT ONLY is allowed?
+					 */
+					RequireTransactionBlock(isTopLevel, "LOCK TABLE");
+				}
+				LockTableCommand(stmt);
+			}
 			break;
 
 		case T_ConstraintsSetStmt:
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 88b03cc472..b35df9bf7a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3774,6 +3774,7 @@ typedef struct LockStmt
 	List	   *relations;		/* relations to lock */
 	int			mode;			/* lock mode */
 	bool		nowait;			/* no wait mode */
+	bool		waitonly;		/* wait only mode */
 } LockStmt;
 
 /* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f5b2e61ca5..827f266492 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -471,6 +471,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/isolation/expected/deadlock-wait-only.out b/src/test/isolation/expected/deadlock-wait-only.out
new file mode 100644
index 0000000000..78b4962fa1
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-only.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1re s2as s2swo s1aewo s1c s2c
+step s1re: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2as: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2swo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step s1aewo: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; <waiting ...>
+step s1aewo: <... completed>
+step s2swo: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-only.out b/src/test/isolation/expected/wait-only.out
new file mode 100644
index 0000000000..3bbd9b68a3
--- /dev/null
+++ b/src/test/isolation/expected/wait-only.out
@@ -0,0 +1,129 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rlwo w2in1 w2c w1c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rl w1c w2c rsel1 rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4fc56ae99c..347cd20d6f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-only
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-only
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-only.spec b/src/test/isolation/specs/deadlock-wait-only.spec
new file mode 100644
index 0000000000..0efca38d60
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-only.spec
@@ -0,0 +1,23 @@
+setup
+{
+  CREATE TABLE a1 ();
+}
+
+teardown
+{
+  DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1re	  { LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1aewo	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; }
+step s1c	  { COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2as		{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2swo	{ LOCK TABLE a1 IN SHARE MODE WAIT ONLY; }
+step s2c		{ COMMIT; }
+
+permutation s1re s2as s2swo s1aewo s1c s2c
diff --git a/src/test/isolation/specs/wait-only.spec b/src/test/isolation/specs/wait-only.spec
new file mode 100644
index 0000000000..6bf4e79058
--- /dev/null
+++ b/src/test/isolation/specs/wait-only.spec
@@ -0,0 +1,61 @@
+setup
+{
+  CREATE TABLE t1 (id bigserial);
+  CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+  DROP TABLE t1;
+  DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1ae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1ae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2c	{ COMMIT; }
+
+session reader
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step rsv	{ SAVEPOINT foo; }
+step rl		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step rrb	{ ROLLBACK TO foo; }
+step rlwo	{ LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; }
+step rsel1	{ SELECT id from t1; }
+step rc		{ COMMIT; }
+
+# reader waits for both writers
+permutation w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+
+# no waiting if writers already committed (obviously)
+permutation w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in1 rlwo w2in1 w2c w1c rsel1 rc
+# ...while actually taking the lock does block writer2 (even if we release it
+# ASAP)
+permutation w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+
+# reader waiting for two tables with only t1 having a conflicting lock doesn't
+# prevent taking an ACCESS EXCLUSIVE lock on t2, or a lesser lock on t1, and the
+# reader doesn't wait for either later lock to be released...
+permutation w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+# ...while actually taking the locks is blocked by the ACCESS EXCLUSIVE lock and
+# would deadlock with the subsequent insert (removed here)
+permutation w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+
+# reader waits only for conflicting lock already held by writer1, not for
+# writer2 which was waiting to take a conflicting lock...
+permutation w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+# ...while actually taking the lock also waits for writer2 to release its lock
+permutation w1ae1 w2in1 rl w1c w2c rsel1 rc
diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out
index ad137d3645..ec5d3f5f4f 100644
--- a/src/test/regress/expected/lock.out
+++ b/src/test/regress/expected/lock.out
@@ -41,6 +41,22 @@ LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+ERROR:  NOWAIT is not supported with WAIT ONLY
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -138,8 +154,22 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ERROR:  permission denied for table lock_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -150,11 +180,21 @@ LOCK TABLE lock_tbl2;
 ERROR:  permission denied for table lock_tbl2
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ERROR:  permission denied for table lock_tbl2
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Tables referred to by views are locked without explicit permission to do so
diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql
index b88488c6d0..0a67bf9681 100644
--- a/src/test/regress/sql/lock.sql
+++ b/src/test/regress/sql/lock.sql
@@ -47,6 +47,24 @@ LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
 
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -104,9 +122,23 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
+
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -116,11 +148,20 @@ BEGIN;
 LOCK TABLE lock_tbl2;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 
-- 
2.34.1

#13Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#12)
1 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Updated docs a bit. I'll see about adding this to the next CF in hopes
of attracting a reviewer. :-)

Attachments:

v3-0001-Add-WAIT-ONLY-option-to-LOCK-command.patchapplication/octet-stream; name=v3-0001-Add-WAIT-ONLY-option-to-LOCK-command.patchDownload
From 80ae2ac293196066702d439ee34b90ff305cbcff Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Mon, 16 Jan 2023 20:51:26 -0800
Subject: [PATCH v3] Add WAIT ONLY option to LOCK command

Rather than actually taking any locks on the table(s), it simply waits
for conflicting lockers using the existing WaitForLockersMultiple()
function in the lock manager.

Unlike when actually taking locks, this obtains the set of conflicting
locks once for all specified tables and then waits for each of those
locks, rather than obtaining conflicts for the first table and waiting
for those before obtaining conflicts for the second table.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

The syntax allows combining it with NOWAIT, which would perhaps be
useful to simply check for conflicts, but this is not yet implemented.
(NOWAIT + immediately releasing the lock already accomplishes roughly
the same thing.)

Unlike other forms of LOCK, WAIT ONLY is allowed outside a transaction
block, since it makes perfect sense to wait and then e.g. SELECT new
data.

Regardless of the specified locking mode, only SELECT permissions are
required on the table(s).
---
 doc/src/sgml/ref/lock.sgml                    |  54 ++++++--
 src/backend/commands/lockcmds.c               |  83 +++++++++--
 src/backend/parser/gram.y                     |  14 +-
 src/backend/tcop/utility.c                    |  24 +++-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 .../isolation/expected/deadlock-wait-only.out |  12 ++
 src/test/isolation/expected/wait-only.out     | 129 ++++++++++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../isolation/specs/deadlock-wait-only.spec   |  23 ++++
 src/test/isolation/specs/wait-only.spec       |  61 +++++++++
 src/test/regress/expected/lock.out            |  40 ++++++
 src/test/regress/sql/lock.sql                 |  41 ++++++
 13 files changed, 458 insertions(+), 27 deletions(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-only.out
 create mode 100644 src/test/isolation/expected/wait-only.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-only.spec
 create mode 100644 src/test/isolation/specs/wait-only.spec

diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index 6ce2518de7..44c46bdbaa 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT ]
+LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN <replaceable class="parameter">lockmode</replaceable> MODE ] [ NOWAIT | WAIT ONLY ]
 
 <phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
 
@@ -42,12 +42,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    error is emitted.  Once obtained, the lock is held for the
    remainder of the current transaction.  (There is no <command>UNLOCK
    TABLE</command> command; locks are always released at transaction
-   end.)
+   end.)  If <literal>WAIT ONLY</literal> is specified, <command>LOCK
+   TABLE</command> simply waits for already-held conflicting locks to be
+   released, and doesn't take any new table-level locks.
   </para>
 
   <para>
    When a view is locked, all relations appearing in the view definition
-   query are also locked recursively with the same lock mode.
+   query are also locked recursively with the same lock mode. Views are not
+   currently supported with <literal>WAIT ONLY</literal>.
   </para>
 
   <para>
@@ -101,6 +104,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    restrictive mode first.
   </para>
 
+  <para>
+   <literal>WAIT ONLY</literal> can be used with <literal>SHARE</literal> mode
+   to wait for in-progress writes to be committed or rolled back, while still
+   allowing other transactions to newly acquire conflicting locks. This can be
+   useful in conjunction with non-transactional communication between clients
+   about writes, such as through sequences. As above, clients must be mindful of
+   isolation levels when observing changes made by other transactions.
+  </para>
+
   <para>
    More information about the lock modes and locking strategies can be
    found in <xref linkend="explicit-locking"/>.
@@ -158,6 +170,24 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term><literal>WAIT ONLY</literal></term>
+    <listitem>
+     <para>
+      Specifies that <command>LOCK TABLE</command> should only wait for existing
+      conflicting locks to be released, and not take any new table-level locks.
+      Unlike when actually locking the tables, this first obtains the combined
+      set of conflicting locks for all specified tables, and then waits on all
+      of those locks (rather than first obtaining the conflicting locks for the
+      first table and waiting on those, then obtaining the conflicting locks for
+      the second table and waiting on those, and so on). Since no table-level
+      locks are taken, a table may be dropped by another transaction while
+      waiting. Views are not currently supported with
+      <literal>WAIT ONLY</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
@@ -175,7 +205,9 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
     MODE</literal> (or a less-conflicting mode as described in <xref
     linkend="explicit-locking"/>) is permitted. If a user has
     <literal>SELECT</literal> privileges on the table, <literal>ACCESS SHARE
-    MODE</literal> is permitted.
+    MODE</literal> is permitted. If <literal>WAIT ONLY</literal> is specified,
+    only <literal>SELECT</literal> privileges are required regardless of
+    <replaceable class="parameter">lockmode</replaceable>.
    </para>
 
    <para>
@@ -191,10 +223,13 @@ LOCK [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ]
    </para>
 
    <para>
-    <command>LOCK TABLE</command> is useless outside a transaction block: the lock
-    would remain held only to the completion of the statement.  Therefore
-    <productname>PostgreSQL</productname> reports an error if <command>LOCK</command>
-    is used outside a transaction block.
+    <command>LOCK TABLE</command> without <literal>WAIT ONLY</literal> is
+    useless outside a transaction block: the lock would remain held only to the
+    completion of the statement, and using <literal>WAIT ONLY</literal> is more
+    efficient if the goal is simply to wait for conflicting locks.  Therefore
+    <productname>PostgreSQL</productname> reports an error if
+    <command>LOCK</command> is used outside a transaction block without
+    <literal>WAIT ONLY</literal>.
     Use
     <link linkend="sql-begin"><command>BEGIN</command></link> and
     <link linkend="sql-commit"><command>COMMIT</command></link>
@@ -262,7 +297,8 @@ COMMIT WORK;
 
   <para>
    Except for <literal>ACCESS SHARE</literal>, <literal>ACCESS EXCLUSIVE</literal>,
-   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes, the
+   and <literal>SHARE UPDATE EXCLUSIVE</literal> lock modes and the
+   <literal>WAIT ONLY</literal> option, the
    <productname>PostgreSQL</productname> lock modes and the
    <command>LOCK TABLE</command> syntax are compatible with those
    present in <productname>Oracle</productname>.
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 40ef4ede26..893f2311b9 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -28,7 +29,8 @@
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
-static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait,
+							 List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -42,6 +44,34 @@ void
 LockTableCommand(LockStmt *lockstmt)
 {
 	ListCell   *p;
+	LOCKMODE	lockmode;
+	LOCKMODE   	waitmode;
+	List	   *waitlocktags = NIL;
+	List	  **waitlocktags_p;
+
+	if (lockstmt->waitonly && lockstmt->nowait)
+		/*
+		 * this could be defined to check and error if there are conflicting
+		 * lockers, but it seems unclear if that would be useful, since
+		 * LOCK ... NOWAIT + immediate unlock would do nearly the same thing
+		 */
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("NOWAIT is not supported with WAIT ONLY")));
+
+
+	if (lockstmt->waitonly)
+	{
+		lockmode = NoLock;
+		waitmode = lockstmt->mode;
+		waitlocktags_p = &waitlocktags;
+	}
+	else
+	{
+		lockmode = lockstmt->mode;
+		waitmode = NoLock;
+		waitlocktags_p = NULL;
+	}
 
 	/*
 	 * Iterate over the list and process the named relations one at a time
@@ -52,16 +82,37 @@ LockTableCommand(LockStmt *lockstmt)
 		bool		recurse = rv->inh;
 		Oid			reloid;
 
-		reloid = RangeVarGetRelidExtended(rv, lockstmt->mode,
+		reloid = RangeVarGetRelidExtended(rv, lockmode,
 										  lockstmt->nowait ? RVR_NOWAIT : 0,
 										  RangeVarCallbackForLockTable,
-										  (void *) &lockstmt->mode);
+										  (void *) &lockmode);
+		if (waitmode != NoLock)
+		{
+			Oid			dbid;
+			LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+
+			if (IsSharedRelation(reloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+			waitlocktags = lappend(waitlocktags, heaplocktag);
+		}
 
 		if (get_rel_relkind(reloid) == RELKIND_VIEW)
-			LockViewRecurse(reloid, lockstmt->mode, lockstmt->nowait, NIL);
+		{
+			if (lockstmt->waitonly || lockmode == NoLock)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("WAIT ONLY is not supported with views")));
+			LockViewRecurse(reloid, lockmode, lockstmt->nowait, NIL);
+		}
 		else if (recurse)
-			LockTableRecurse(reloid, lockstmt->mode, lockstmt->nowait);
+			LockTableRecurse(reloid, lockmode, lockstmt->nowait,
+							 waitlocktags_p);
 	}
+	if (waitmode != NoLock)
+		WaitForLockersMultiple(waitlocktags, waitmode, false);
 }
 
 /*
@@ -115,7 +166,7 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
  * parent which is enough.
  */
 static void
-LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
+LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait, List **locktags_p)
 {
 	List	   *children;
 	ListCell   *lc;
@@ -125,11 +176,26 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	foreach(lc, children)
 	{
 		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
 
-		/* Parent already locked. */
+		/* Parent already handled. */
 		if (childreloid == reloid)
 			continue;
 
+		if (locktags_p != NULL)
+		{
+			heaplocktag = palloc_object(LOCKTAG);
+			if (IsSharedRelation(childreloid))
+				dbid = InvalidOid;
+			else
+				dbid = MyDatabaseId;
+			SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+			*locktags_p = lappend(*locktags_p, heaplocktag);
+		}
+
+		if (lockmode == NoLock)
+			continue;
 		if (!nowait)
 			LockRelationOid(childreloid, lockmode);
 		else if (!ConditionalLockRelationOid(childreloid, lockmode))
@@ -228,7 +294,8 @@ LockViewRecurse_walker(Node *node, LockViewRecurse_context *context)
 				LockViewRecurse(relid, context->lockmode, context->nowait,
 								context->ancestor_views);
 			else if (rte->inh)
-				LockTableRecurse(relid, context->lockmode, context->nowait);
+				LockTableRecurse(relid, context->lockmode, context->nowait,
+								 NULL);
 		}
 
 		return query_tree_walker(query,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 15ece871a0..c62693cf7a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -353,7 +353,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_waitonly opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -12146,13 +12146,14 @@ using_clause:
  *
  *****************************************************************************/
 
-LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait
+LockStmt:	LOCK_P opt_table relation_expr_list opt_lock opt_nowait opt_waitonly
 				{
 					LockStmt   *n = makeNode(LockStmt);
 
 					n->relations = $3;
 					n->mode = $4;
 					n->nowait = $5;
+					n->waitonly = $6;
 					$$ = (Node *) n;
 				}
 		;
@@ -12175,6 +12176,11 @@ opt_nowait:	NOWAIT							{ $$ = true; }
 			| /*EMPTY*/						{ $$ = false; }
 		;
 
+opt_waitonly:
+			WAIT ONLY						{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
 opt_nowait_or_skip:
 			NOWAIT							{ $$ = LockWaitError; }
 			| SKIP LOCKED					{ $$ = LockWaitSkip; }
@@ -17301,6 +17307,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17932,6 +17939,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index e3ccf6c7f7..423731cd78 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -354,7 +354,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 				 * restrictions here must match those in
 				 * LockAcquireExtended().
 				 */
-				if (stmt->mode > RowExclusiveLock)
+				if (!stmt->waitonly && stmt->mode > RowExclusiveLock)
 					return COMMAND_OK_IN_READ_ONLY_TXN;
 				else
 					return COMMAND_IS_STRICTLY_READ_ONLY;
@@ -932,13 +932,23 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			break;
 
 		case T_LockStmt:
+			{
+				LockStmt *stmt = (LockStmt *) parsetree;
 
-			/*
-			 * Since the lock would just get dropped immediately, LOCK TABLE
-			 * outside a transaction block is presumed to be user error.
-			 */
-			RequireTransactionBlock(isTopLevel, "LOCK TABLE");
-			LockTableCommand((LockStmt *) parsetree);
+				if (!stmt->waitonly)
+				{
+					/*
+					 * Since the lock would just get dropped immediately, and
+					 * simply waiting is better done with WAIT ONLY, LOCK TABLE
+					 * without WAIT ONLY outside a transaction block is presumed
+					 * to be user error.
+					 *
+					 * XXX: the error should clarify that WAIT ONLY is allowed?
+					 */
+					RequireTransactionBlock(isTopLevel, "LOCK TABLE");
+				}
+				LockTableCommand(stmt);
+			}
 			break;
 
 		case T_ConstraintsSetStmt:
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fe003ded50..47badd29ae 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3813,6 +3813,7 @@ typedef struct LockStmt
 	List	   *relations;		/* relations to lock */
 	int			mode;			/* lock mode */
 	bool		nowait;			/* no wait mode */
+	bool		waitonly;		/* wait only mode */
 } LockStmt;
 
 /* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..9ae555d6b7 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -473,6 +473,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/isolation/expected/deadlock-wait-only.out b/src/test/isolation/expected/deadlock-wait-only.out
new file mode 100644
index 0000000000..78b4962fa1
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-only.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1re s2as s2swo s1aewo s1c s2c
+step s1re: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2as: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2swo: LOCK TABLE a1 IN SHARE MODE WAIT ONLY; <waiting ...>
+step s1aewo: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; <waiting ...>
+step s1aewo: <... completed>
+step s2swo: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-only.out b/src/test/isolation/expected/wait-only.out
new file mode 100644
index 0000000000..3bbd9b68a3
--- /dev/null
+++ b/src/test/isolation/expected/wait-only.out
@@ -0,0 +1,129 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rlwo w2in1 w2c w1c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1ae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rlwo: LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step rlwo: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1ae1 w2in1 rl w1c w2c rsel1 rc
+step w1ae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4fc56ae99c..347cd20d6f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-only
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-only
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-only.spec b/src/test/isolation/specs/deadlock-wait-only.spec
new file mode 100644
index 0000000000..0efca38d60
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-only.spec
@@ -0,0 +1,23 @@
+setup
+{
+  CREATE TABLE a1 ();
+}
+
+teardown
+{
+  DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1re	  { LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1aewo	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE WAIT ONLY; }
+step s1c	  { COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2as		{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2swo	{ LOCK TABLE a1 IN SHARE MODE WAIT ONLY; }
+step s2c		{ COMMIT; }
+
+permutation s1re s2as s2swo s1aewo s1c s2c
diff --git a/src/test/isolation/specs/wait-only.spec b/src/test/isolation/specs/wait-only.spec
new file mode 100644
index 0000000000..9a4e39cb9c
--- /dev/null
+++ b/src/test/isolation/specs/wait-only.spec
@@ -0,0 +1,61 @@
+setup
+{
+  CREATE TABLE t1 (id bigserial);
+  CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+  DROP TABLE t1;
+  DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1ae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1ae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2c	{ COMMIT; }
+
+session reader
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step rsv	{ SAVEPOINT foo; }
+step rl		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step rrb	{ ROLLBACK TO foo; }
+step rlwo	{ LOCK TABLE t1, t2 IN SHARE MODE WAIT ONLY; }
+step rsel1	{ SELECT id from t1; }
+step rc		{ COMMIT; }
+
+# reader waits for both writers
+permutation w1ae2 w2in1 rlwo w2c w1c rsel1 rc
+
+# no waiting if writers already committed (obviously)
+permutation w1ae2 w2in1 w2c w1c rlwo rsel1 rc
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in1 rlwo w2in1 w2c w1c rsel1 rc
+# ...while actually taking the lock does block writer2 (even if we release it
+# ASAP)
+permutation w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+
+# reader waiting for two tables with only t1 having a conflicting lock doesn't
+# prevent taking an ACCESS EXCLUSIVE lock on t2, or a lesser lock on t1, and the
+# reader doesn't wait for either later lock to be released...
+permutation w2in1 rlwo w1ae2 w1in1 w2c rsel1 w1c rc
+# ...while actually taking the locks is blocked by the ACCESS EXCLUSIVE lock and
+# would deadlock with the subsequent insert w1in1 (removed here)
+permutation w2in1 rsv rl w1ae2 w2c w1c rrb rsel1 rc
+
+# reader waits only for conflicting lock already held by writer1, not for
+# writer2 which was waiting to take a conflicting lock...
+permutation w1ae1 w2in1 rlwo w1c rsel1 w2c rc
+# ...while actually taking the lock also waits for writer2 to release its lock
+permutation w1ae1 w2in1 rl w1c w2c rsel1 rc
diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out
index ad137d3645..ec5d3f5f4f 100644
--- a/src/test/regress/expected/lock.out
+++ b/src/test/regress/expected/lock.out
@@ -41,6 +41,22 @@ LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+ERROR:  NOWAIT is not supported with WAIT ONLY
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -138,8 +154,22 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ERROR:  permission denied for table lock_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -150,11 +180,21 @@ LOCK TABLE lock_tbl2;
 ERROR:  permission denied for table lock_tbl2
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ERROR:  permission denied for table lock_tbl2
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 -- Tables referred to by views are locked without explicit permission to do so
diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql
index b88488c6d0..0a67bf9681 100644
--- a/src/test/regress/sql/lock.sql
+++ b/src/test/regress/sql/lock.sql
@@ -47,6 +47,24 @@ LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE NOWAIT;
 LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT;
 ROLLBACK;
 
+-- Try using WAIT ONLY along with valid options.
+BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE UPDATE EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN SHARE ROW EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN EXCLUSIVE MODE WAIT ONLY;
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+
+-- WAIT ONLY is allowed outside a transaction
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+
+-- NOWAIT + WAIT ONLY is not supported (yet?)
+LOCK TABLE lock_tbl1 IN ACCESS EXCLUSIVE MODE NOWAIT WAIT ONLY;
+
 -- Verify that we can lock views.
 BEGIN TRANSACTION;
 LOCK TABLE lock_view1 IN EXCLUSIVE MODE;
@@ -104,9 +122,23 @@ ROLLBACK;
 CREATE TABLE lock_tbl2 (b BIGINT) INHERITS (lock_tbl1);
 CREATE TABLE lock_tbl3 () INHERITS (lock_tbl2);
 BEGIN TRANSACTION;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 
+-- WAIT ONLY requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS SHARE MODE WAIT ONLY;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE lock_tbl1 TO regress_rol_lock1;
+LOCK TABLE ONLY lock_tbl1 IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+RESET ROLE;
+REVOKE SELECT ON TABLE lock_tbl1 FROM regress_rol_lock1;
+
 -- Child tables are locked without granting explicit permission to do so as
 -- long as we have permission to lock the parent.
 GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
@@ -116,11 +148,20 @@ BEGIN;
 LOCK TABLE lock_tbl2;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl2 WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE;
 ROLLBACK;
 BEGIN;
+LOCK TABLE lock_tbl1 * IN ACCESS EXCLUSIVE MODE WAIT ONLY;
+ROLLBACK;
+BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
+BEGIN;
+LOCK TABLE ONLY lock_tbl1 WAIT ONLY;
+ROLLBACK;
 RESET ROLE;
 REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
 
-- 
2.34.1

#14Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#13)
Re: Exposing the lock manager's WaitForLockers() to SQL

I realized that for our use case, we'd ideally wait for holders of
RowExclusiveLock only, and not e.g. VACUUM holding
ShareUpdateExclusiveLock. Waiting for lockers in a specific mode seems
possible by generalizing/duplicating WaitForLockersMultiple() and
GetLockConflicts(), but I'd love to have a sanity check before
attempting that. Also, I imagine those semantics might be too
different to make sense as part of the LOCK command.

Alternatively, I had originally been trying to use the pg_locks view,
which obviously provides flexibility in identifying existing lock
holders. But I couldn't find a way to wait for the locks to be
released / transactions to finish, and I was a little concerned about
the performance impact of selecting from it frequently when we only
care about a subset of the locks, although I didn't try to assess that
in our particular application.

In any case, I'm looking forward to hearing more feedback from
reviewers and potential users. :-)

#15Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#14)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Sun, Sep 3, 2023 at 11:16 PM Will Mortensen <will@extrahop.com> wrote:

I realized that for our use case, we'd ideally wait for holders of
RowExclusiveLock only, and not e.g. VACUUM holding
ShareUpdateExclusiveLock. Waiting for lockers in a specific mode seems
possible by generalizing/duplicating WaitForLockersMultiple() and
GetLockConflicts(), but I'd love to have a sanity check before
attempting that. Also, I imagine those semantics might be too
different to make sense as part of the LOCK command.

Well I attempted it. :-) Here is a new series that refactors
GetLockConflicts(), generalizes WaitForLockersMultiple(), and adds a
new WAIT FOR LOCKERS command.

I first tried extending LOCK further, but the code became somewhat
unwieldy and the syntax became very confusing. I also thought again
about making new pg_foo() functions, but that would seemingly make it
harder to share code with LOCK, and sharing syntax (to the extent it
makes sense) feels very natural. Also, a new SQL command provides
plenty of doc space. :-) (I'll see about adding more examples later.)

I'll try to edit the title of the CF entry accordingly. Still looking
forward to any feedback. :-)

Attachments:

v4-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v4-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From 5fe773750199b4087290e821a9312d39b1e4ab4f Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v4 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lockmode, rather than all lockmodes
that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 29 ++++++++++++++++-------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7b186c0220..6481f6c989 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2290,7 +2290,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2306,7 +2306,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index e56205abd8..c2a72944d9 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1660,7 +1660,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1707,7 +1707,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -3924,7 +3924,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -3983,7 +3983,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4141,7 +4141,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4175,7 +4175,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b0a20010e..f654371b1e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19284,7 +19284,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index b447ddf11b..6a1e0defd3 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -920,11 +921,13 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 	foreach(lc, locktags)
 	{
 		LOCKTAG    *locktag = lfirst(lc);
+		LOCKMASK	lockmask = conflicting ?
+			GetLockConflictMask(locktag, lockmode) : LOCKBIT_ON(lockmode);
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmask,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -933,8 +936,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +986,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 39f0e346b0..e896f0cba5 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v4-0003-Add-WAIT-FOR-LOCKERS-command.patchapplication/octet-stream; name=v4-0003-Add-WAIT-FOR-LOCKERS-command.patchDownload
From 00d3415953879fdc4e89527ee6c4a6f2a9996a3c Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Sat, 23 Dec 2023 01:42:57 -0800
Subject: [PATCH v4 3/3] Add WAIT FOR LOCKERS command

Rather than actually taking any locks on the table(s), it simply waits
for existing lockers using the existing WaitForLockersMultiple()
function in the lock manager.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

See docs and tests for more detail.
---
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/wait_for_lockers.sgml        | 271 ++++++++++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/lockcmds.c               |  77 +++++
 src/backend/parser/gram.y                     |  51 +++-
 src/backend/tcop/utility.c                    |  18 ++
 src/include/commands/lockcmds.h               |   5 +
 src/include/nodes/parsenodes.h                |  12 +
 src/include/parser/kwlist.h                   |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/deadlock-wait-for-lockers.out    |  12 +
 .../isolation/expected/wait-for-lockers.out   | 144 ++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/deadlock-wait-for-lockers.spec      |  23 ++
 .../isolation/specs/wait-for-lockers.spec     |  64 +++++
 .../regress/expected/wait_for_lockers.out     |  87 ++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  93 ++++++
 18 files changed, 861 insertions(+), 5 deletions(-)
 create mode 100644 doc/src/sgml/ref/wait_for_lockers.sgml
 create mode 100644 src/test/isolation/expected/deadlock-wait-for-lockers.out
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-for-lockers.spec
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index fda4690eab..f40be2fd0e 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -188,6 +188,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY update             SYSTEM "update.sgml">
 <!ENTITY vacuum             SYSTEM "vacuum.sgml">
 <!ENTITY values             SYSTEM "values.sgml">
+<!ENTITY waitForLockers     SYSTEM "wait_for_lockers.sgml">
 
 <!-- applications and utilities -->
 <!ENTITY clusterdb          SYSTEM "clusterdb.sgml">
diff --git a/doc/src/sgml/ref/wait_for_lockers.sgml b/doc/src/sgml/ref/wait_for_lockers.sgml
new file mode 100644
index 0000000000..2ea49c64f3
--- /dev/null
+++ b/doc/src/sgml/ref/wait_for_lockers.sgml
@@ -0,0 +1,271 @@
+<!--
+doc/src/sgml/ref/wait_for_lockers.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-waitforlockers">
+ <indexterm zone="sql-waitforlockers">
+  <primary>WAIT FOR LOCKERS</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>WAIT FOR LOCKERS</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>WAIT FOR LOCKERS</refname>
+  <refpurpose>wait for table locks to be released</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+WAIT FOR LOCKERS OF [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN [ CONFLICT WITH ] <replaceable class="parameter">lockmode</replaceable> MODE ]
+
+<phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
+
+    ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
+    | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> waits for already-held table-level locks
+   to be released.  It does not wait for locks that are taken after it starts
+   waiting, even if the locker was already waiting to take the lock.  It does
+   not take any table-level locks.
+  </para>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> can be used either inside or outside a
+   transaction block. Within a transaction at the
+   <literal>REPEATABLE READ</literal> or <literal>SERIALIZABLE</literal>
+   isolation level, in order to observe any changes made by the waited-for
+   lockers, you have to execute the <command>WAIT FOR LOCKERS</command>
+   statement before executing any <command>SELECT</command> or data modification
+   statement.  A <literal>REPEATABLE READ</literal> or
+   <literal>SERIALIZABLE</literal> transaction's view of data will be frozen
+   when its first <command>SELECT</command> or data modification statement
+   begins.  A <command>WAIT FOR LOCKERS</command> later in the transaction will
+   still wait for outstanding writes &mdash; but it won't ensure that what the
+   transaction reads afterward corresponds to the latest committed values.
+  </para>
+
+  <para>
+   Since <command>WAIT FOR LOCKERS</command> does not take any table-level
+   locks, a table may be dropped by another transaction while waiting. Once the
+   drop commits, there cannot be any remaining locks on the table to wait for.
+  </para>
+
+  <para>
+   As with any command that potentially waits for other transactions,
+   <command>WAIT FOR LOCKERS</command> can participate in deadlocks if it waits
+   for another transaction that transitively waits for its transaction, although
+   this is generally less likely than when actually taking a lock. Also note
+   that a transaction will not wait for its own locks, just as its own locks do
+   not conflict.
+  </para>
+
+  <para>
+   More information about the lock modes and how they conflict can be
+   found in <xref linkend="explicit-locking"/>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing table on which to
+      wait. If <literal>ONLY</literal> is specified before the table name, only
+      that table is waited for. If <literal>ONLY</literal> is not specified, the
+      table and all its descendant tables (if any) are waited for.  Optionally,
+      <literal>*</literal> can be specified after the table name to explicitly
+      indicate that descendant tables are included.
+     </para>
+
+     <para>
+      With multiple tables, <command>WAIT FOR LOCKERS</command> first obtains
+      the combined set of matching locks for all tables, and then waits on all
+      of those locks. This differs from the behavior of
+      <xref linkend="sql-lock"/> with multiple tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IN</literal></term>
+    <term><literal>IN CONFLICT WITH</literal></term>
+    <listitem>
+     <para>
+      Specifies whether to wait for locks only in exactly the following
+      <replaceable class="parameter">lockmode</replaceable>, or in all modes
+      that conflict with <replaceable class="parameter">lockmode</replaceable>
+      (note that <replaceable class="parameter">lockmode</replaceable> may or
+      may not conflict with itself).  Lock modes and their conflicts are
+      described in <xref linkend="explicit-locking"/>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">lockmode</replaceable></term>
+    <listitem>
+     <para>
+      The lock mode (in conjunction with the previous clause) determines which
+      locks to wait for. If no lock mode is specified, then
+      <literal>IN CONFLICT WITH ACCESS EXCLUSIVE</literal>, the most restrictive
+      set of options, is used.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   To wait for locks on a table, the user must have <literal>SELECT</literal>
+   privileges on the table.
+  </para>
+
+  <para>
+   Views are not currently supported.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Example</title>
+
+  <para>
+   <literal>IN ROW EXCLUSIVE MODE</literal> or
+   <literal>IN CONFLICT WITH SHARE MODE</literal> can be used to wait for
+   in-progress writes to be committed or rolled back, while still allowing other
+   transactions to newly acquire conflicting locks and perform writes.  This can
+   be useful in conjunction with non-transactional communication about
+   in-progress writes, such as through sequences.
+  </para>
+
+  <para>
+   For example, imagine we have a table of page views:
+
+<programlisting>
+CREATE TABLE page_views (
+    id bigserial,
+    view_time timestamptz default now(),
+    domain text
+);
+</programlisting>
+  </para>
+
+  <para>
+   And say that we want to maintain a separate table of view counts rolled up by
+   <literal>domain</literal> for fast retrieval, but we don't want to update it
+   on every page view:
+
+<programlisting>
+CREATE TABLE page_view_rollup (
+    domain text primary key,
+    view_count bigint
+);
+</programlisting>
+  </para>
+
+  <para>
+   Assume that the <literal>page_views</literal> table is only ever modified by
+   <command>INSERT</command> commands that assign the default value to the
+   <literal>id</literal> column, which is taken from the automatically-created
+   <literal>page_views_id_seq</literal> sequence, which is uncached. Then we
+   know that the values of <literal>id</literal> in the rows being inserted are
+   obtained from the sequence after <command>INSERT</command> takes its
+   <literal>ROW EXCLUSIVE</literal> lock on <literal>page_views</literal>. When
+   generating a rollup, we can call the
+   <literal>pg_sequence_last_value</literal> function to read the current value
+   of the sequence, and then execute
+   <literal>WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;</literal>
+   to wait for outstanding <command>INSERT</command> commands to commit or roll
+   back. After it finishes waiting, we know that all rows with
+   <literal>id</literal> values less than or equal to the sequence value that we
+   read have been committed or rolled back, i.e., any outstanding uncommitted
+   rows must have a greater value for <literal>id</literal>.
+  </para>
+
+  <para>
+   We can use this knowledge to process each row exactly once for our rollup
+   table. We update the rollup table by running the following function inside a
+   transaction at the <literal>READ COMMITTED</literal> isolation level:
+
+<programlisting>
+CREATE TABLE page_view_rollup_state (
+    last_id bigint
+);
+
+CREATE FUNCTION update_page_view_rollup()
+RETURNS VOID
+LANGUAGE plpgsql
+AS $$
+DECLARE
+    _last_id bigint;
+    _cur_id bigint;
+BEGIN
+    DELETE FROM page_view_rollup_state RETURNING last_id INTO _last_id;
+
+    SELECT pg_sequence_last_value('page_views_id_seq') INTO _cur_id;
+
+    WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+
+    -- We know that any page_views where id &lt;= _cur_id have been either
+    -- committed or rolled back. For the same reason, we know that any
+    -- page_views where id &lt;= _last_id were processed in a previous invocation
+    -- (or rolled back). There may be some page_views where id &gt; cur_id that
+    -- have already been committed, but more may be committed later, so we'll
+    -- process them in a future invocation to avoid double-counting.
+    INSERT INTO page_view_rollup (domain, view_count)
+        SELECT domain, count(*) as view_count
+        FROM page_views
+        WHERE id &gt; coalesce(_last_id, 0) AND id &lt;= _cur_id
+        GROUP BY domain
+    ON CONFLICT (domain) DO UPDATE
+    SET view_count = page_view_rollup.view_count + excluded.view_count;
+
+    INSERT INTO page_view_rollup_state (last_id) VALUES (_cur_id);
+END;
+$$;
+</programlisting>
+  </para>
+
+  <para>
+   In this example, a transaction is needed because the function temporarily
+   deletes from <literal>page_view_rollup_state</literal>, and the transaction
+   must use the <literal>READ COMMITTED</literal> isolation level because it
+   runs <command>DELETE</command> and <command>SELECT</command> before
+   <command>WAIT FOR LOCKERS</command> but expects newly committed rows to be
+   visible to the sub-<command>SELECT</command> afterward.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>WAIT FOR LOCKERS</command> in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-lock"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a07d2b5e01..cd827a12ca 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -216,6 +216,7 @@
    &update;
    &vacuum;
    &values;
+   &waitForLockers;
 
  </reference>
 
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 40ef4ede26..dacecc5f13 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -29,6 +30,7 @@
 #include "utils/syscache.h"
 
 static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static void GetLocktagsRecurse(Oid reloid, List **locktags_p);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -64,6 +66,47 @@ LockTableCommand(LockStmt *lockstmt)
 	}
 }
 
+/*
+ * WAIT FOR LOCKERS
+ */
+void
+WaitForLockersCommand(WaitForLockersStmt *waitstmt)
+{
+	ListCell   *p;
+	List	   *locktags = NIL;
+
+	/*
+	 * Iterate over the list and process the named relations one at a time
+	 */
+	foreach(p, waitstmt->relations)
+	{
+		RangeVar   *rv = (RangeVar *) lfirst(p);
+		bool		recurse = rv->inh;
+		LOCKTAG	   *heaplocktag = palloc_object(LOCKTAG);
+		LOCKMODE	nolock = NoLock;
+		Oid			reloid;
+		Oid			dbid;
+
+		reloid = RangeVarGetRelidExtended(rv, NoLock, 0,
+										  RangeVarCallbackForLockTable,
+										  (void *) &nolock);
+		if (get_rel_relkind(reloid) == RELKIND_VIEW)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("WAIT FOR LOCKERS is not supported with views")));
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+		locktags = lappend(locktags, heaplocktag);
+		if (recurse)
+			GetLocktagsRecurse(reloid, &locktags);
+	}
+	WaitForLockersMultiple(locktags, waitstmt->mode, waitstmt->conflicting,
+						   false);
+}
+
 /*
  * Before acquiring a table lock on the named table, check whether we have
  * permission to do so.
@@ -158,6 +201,40 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	}
 }
 
+/*
+ * Get locktags recursively over an inheritance tree
+ *
+ * This doesn't check permission on the child tables, because getting here means
+ * that the user has permission on the parent which is enough.
+ */
+static void
+GetLocktagsRecurse(Oid reloid, List **locktags_p)
+{
+	List	   *children;
+	ListCell   *lc;
+
+	children = find_all_inheritors(reloid, NoLock, NULL);
+
+	foreach(lc, children)
+	{
+		Oid			childreloid = lfirst_oid(lc);
+		Oid			dbid;
+		LOCKTAG	   *heaplocktag;
+
+		/* Parent already handled. */
+		if (childreloid == reloid)
+			continue;
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(childreloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, childreloid);
+		*locktags_p = lappend(*locktags_p, heaplocktag);
+	}
+}
+
 /*
  * Apply LOCK TABLE recursively over a view
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 63f172e175..f24447429b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -319,6 +319,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateMatViewStmt RefreshMatViewStmt CreateAmStmt
 		CreatePublicationStmt AlterPublicationStmt
 		CreateSubscriptionStmt AlterSubscriptionStmt DropSubscriptionStmt
+		WaitForLockersStmt
 
 %type <node>	select_no_parens select_with_parens select_clause
 				simple_select values_clause
@@ -345,7 +346,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				transaction_mode_item
 				create_extension_opt_item alter_extension_opt_item
 
-%type <ival>	opt_lock lock_type cast_context
+%type <ival>	opt_lock lock_type opt_wait_lock_modes cast_context
 %type <str>		utility_option_name
 %type <defelt>	utility_option_elem
 %type <list>	utility_option_list
@@ -353,7 +354,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_conflict_with opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -729,7 +730,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
-	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
+	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOCKERS LOGGED
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -773,7 +774,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -1102,6 +1103,7 @@ stmt:
 			| VariableSetStmt
 			| VariableShowStmt
 			| ViewStmt
+			| WaitForLockersStmt
 			| /*EMPTY*/
 				{ $$ = NULL; }
 		;
@@ -12257,6 +12259,43 @@ opt_nowait_or_skip:
 		;
 
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				WAIT FOR LOCKERS
+ *
+ *****************************************************************************/
+
+WaitForLockersStmt:
+			WAIT FOR LOCKERS OF opt_table relation_expr_list opt_wait_lock_modes
+				{
+					WaitForLockersStmt *n = makeNode(WaitForLockersStmt);
+
+					n->relations = $6;
+					/* XXX: see opt_wait_lock_modes */
+					n->mode = $7 & ((1 << MaxLockMode) - 1);
+					n->conflicting = ($7 >> MaxLockMode) != 0;
+					$$ = (Node *) n;
+				}
+		;
+
+opt_wait_lock_modes:
+			/*
+			 * XXX: hackily store a bool and a lock mode in an int; should
+			 * probably make a new Node type?
+			 */
+			IN_P opt_conflict_with lock_type MODE
+					{ $$ = ((int)$2 << MaxLockMode) | $3; }
+			| /*EMPTY*/
+					{ $$ = (1 << MaxLockMode) | AccessExclusiveLock; }
+		;
+
+opt_conflict_with:
+			CONFLICT WITH					{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17255,6 +17294,7 @@ unreserved_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -17414,6 +17454,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17845,6 +17886,7 @@ bare_label_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -18045,6 +18087,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 366a27ae8e..960d04c220 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -360,6 +360,11 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 					return COMMAND_IS_STRICTLY_READ_ONLY;
 			}
 
+		case T_WaitForLockersStmt:
+			{
+				return COMMAND_IS_STRICTLY_READ_ONLY;
+			}
+
 		case T_TransactionStmt:
 			{
 				TransactionStmt *stmt = (TransactionStmt *) parsetree;
@@ -941,6 +946,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			LockTableCommand((LockStmt *) parsetree);
 			break;
 
+		case T_WaitForLockersStmt:
+
+			WaitForLockersCommand((WaitForLockersStmt *) parsetree);
+			break;
+
 		case T_ConstraintsSetStmt:
 			WarnNoTransactionBlock(isTopLevel, "SET CONSTRAINTS");
 			AfterTriggerSetState((ConstraintsSetStmt *) parsetree);
@@ -2993,6 +3003,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_LOCK_TABLE;
 			break;
 
+		case T_WaitForLockersStmt:
+			tag = CMDTAG_WAIT_FOR_LOCKERS;
+			break;
+
 		case T_ConstraintsSetStmt:
 			tag = CMDTAG_SET_CONSTRAINTS;
 			break;
@@ -3614,6 +3628,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_WaitForLockersStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ConstraintsSetStmt:
 			lev = LOGSTMT_ALL;
 			break;
diff --git a/src/include/commands/lockcmds.h b/src/include/commands/lockcmds.h
index 6c298c71b3..9807099aaa 100644
--- a/src/include/commands/lockcmds.h
+++ b/src/include/commands/lockcmds.h
@@ -21,4 +21,9 @@
  */
 extern void LockTableCommand(LockStmt *lockstmt);
 
+/*
+ * WAIT FOR LOCKERS
+ */
+extern void WaitForLockersCommand(WaitForLockersStmt *lockstmt);
+
 #endif							/* LOCKCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e494309da8..9a1da11a72 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3797,6 +3797,18 @@ typedef struct LockStmt
 	bool		nowait;			/* no wait mode */
 } LockStmt;
 
+/* ----------------------
+ *		WAIT FOR LOCKERS Statement
+ * ----------------------
+ */
+typedef struct WaitForLockersStmt
+{
+	NodeTag		type;
+	List	   *relations;		/* relations to wait for */
+	int			mode;			/* lock mode */
+	bool		conflicting;	/* conflicting lock modes */
+} WaitForLockersStmt;
+
 /* ----------------------
  *		SET CONSTRAINTS Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..4be2b81f41 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -259,6 +259,7 @@ PG_KEYWORD("localtimestamp", LOCALTIMESTAMP, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("location", LOCATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("lockers", LOCKERS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -473,6 +474,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 320ee91512..55317b98f9 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -217,3 +217,4 @@ PG_CMDTAG(CMDTAG_TRUNCATE_TABLE, "TRUNCATE TABLE", false, false, false)
 PG_CMDTAG(CMDTAG_UNLISTEN, "UNLISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_UPDATE, "UPDATE", false, false, true)
 PG_CMDTAG(CMDTAG_VACUUM, "VACUUM", false, false, false)
+PG_CMDTAG(CMDTAG_WAIT_FOR_LOCKERS, "WAIT FOR LOCKERS", false, false, false)
diff --git a/src/test/isolation/expected/deadlock-wait-for-lockers.out b/src/test/isolation/expected/deadlock-wait-for-lockers.out
new file mode 100644
index 0000000000..2241a7e999
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-for-lockers.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1lre s2las s2wfl s1wfl s1c s2c
+step s1lre: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2las: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step s1wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; <waiting ...>
+step s1wfl: <... completed>
+step s2wfl: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..e5e49c3f88
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,144 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1lae2 w2in1 rwfl w2c rsel1 w1c rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwfl: WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; <waiting ...>
+step w2c: COMMIT;
+step rwfl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1lae2 w2in1 rwflic w2c w1c rsel1 rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1lae2 w2in1 w2c w1c rwflic rsel1 rc
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rwflic w2in1 w2c w1c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step rc: COMMIT;
+
+starting permutation: w2in1 rwflic w1lae2 w1in1 w2c rsel1 w1c rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2c: COMMIT;
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w2in1 rsv rl w1lae2 w2c w1c rrb rsel1 rc
+step w2in1: INSERT INTO t1 VALUES (DEFAULT);
+step rsv: SAVEPOINT foo;
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2c: COMMIT;
+step w1c: COMMIT;
+step rl: <... completed>
+step rrb: ROLLBACK TO foo;
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
+
+starting permutation: w1lae1 w2in1 rwflic w1c rsel1 w2c rc
+step w1lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rwflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step rwflic: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2c: COMMIT;
+step rc: COMMIT;
+
+starting permutation: w1lae1 w2in1 rl w1c w2c rsel1 rc
+step w1lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step rl: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1c: COMMIT;
+step w2in1: <... completed>
+step w2c: COMMIT;
+step rl: <... completed>
+step rsel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step rc: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..b7380627d7 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-for-lockers
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-for-lockers.spec b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
new file mode 100644
index 0000000000..1b34e3cb9b
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
@@ -0,0 +1,23 @@
+setup
+{
+	CREATE TABLE a1 ();
+}
+
+teardown
+{
+	DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1lre	{ LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2las	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; }
+step s2c	{ COMMIT; }
+
+permutation s1lre s2las s2wfl s1wfl s1c s2c
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..f6fbfcff38
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,64 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2c	{ COMMIT; }
+
+session reader
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step rsv	{ SAVEPOINT foo; }
+step rl		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step rrb	{ ROLLBACK TO foo; }
+step rwfl	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; }
+step rwflic	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; }
+step rsel1	{ SELECT id from t1; }
+step rc		{ COMMIT; }
+
+# reader waits only for writer in ROW EXCLUSIVE mode
+permutation w1lae2 w2in1 rwfl w2c rsel1 w1c rc
+# reader waits for both writers conflicting with SHARE
+permutation w1lae2 w2in1 rwflic w2c w1c rsel1 rc
+
+# no waiting if writers already committed (obviously)
+permutation w1lae2 w2in1 w2c w1c rwflic rsel1 rc
+
+# reader waiting for writer1 doesn't block writer2...
+permutation w1in1 rwflic w2in1 w2c w1c rsel1 rc
+# ...while actually taking the lock does block writer2 (even if we release it
+# ASAP)
+permutation w1in1 rsv rl w2in1 w1c rrb w2c rsel1 rc
+
+# reader waiting for two tables with only t1 having a conflicting lock doesn't
+# prevent taking an ACCESS EXCLUSIVE lock on t2, or a lesser lock on t1, and the
+# reader doesn't wait for either later lock to be released...
+permutation w2in1 rwflic w1lae2 w1in1 w2c rsel1 w1c rc
+# ...while actually taking the locks is blocked by the later ACCESS EXCLUSIVE
+# lock and would deadlock with the subsequent insert w1in1 (removed here)
+permutation w2in1 rsv rl w1lae2 w2c w1c rrb rsel1 rc
+
+# reader waits only for conflicting lock already held by writer1, not for
+# writer2 which was waiting to take a conflicting lock...
+permutation w1lae1 w2in1 rwflic w1c rsel1 w2c rc
+# ...while actually taking the lock also waits for writer2 to release its lock
+permutation w1lae1 w2in1 rl w1c w2c rsel1 rc
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..f1e324ba0c
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,87 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ERROR:  permission denied for table wfl_tbl2
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..d2ec0a6a86 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..732302331e
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,93 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

v4-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v4-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 7b46415f315b1cbb708063b4a5ce6eb381e5e1b6 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v4 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

GetLockers() supports getting lockers in modes matching an arbitrary
lockmask, not just those that conflict with a specific lockmode.
GetLockConflicts() is still available as a convenience wrapper, and its
semantics are unchanged.

Also factor out and export GetLockConflictMask() for use in a later
commit.
---
 src/backend/storage/lmgr/lock.c | 91 +++++++++++++++++++++++----------
 src/include/storage/lock.h      |  3 ++
 2 files changed, 67 insertions(+), 27 deletions(-)

diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index b8c57b3e16..bc7ae70535 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2832,45 +2832,78 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 	return proclock;
 }
 
+/*
+ * GetLockConflictMask
+ *		Return the LOCKMASK of lockmodes that would conflict with the given
+ *		lockmode if taken on locktag.
+ */
+LOCKMASK
+GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode)
+{
+	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
+	LockMethod	lockMethodTable;
+
+	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
+		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
+	lockMethodTable = LockMethods[lockmethodid];
+	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	return lockMethodTable->conflictTab[lockmode];
+}
+
 /*
  * GetLockConflicts
+ *
+ * Convenience wrapper for GetLockers conflicting with a single lockmode.
+ */
+VirtualTransactionId *
+GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+{
+	return GetLockers(locktag, GetLockConflictMask(locktag, lockmode), countp);
+}
+
+/*
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag and matching the specified getlockmask, which
+ *		is assumed to be valid for locktag. xacts merely awaiting such a lock
+ *		are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMASK getlockmask, int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
-		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	numLockModes = lockMethodTable->numLockModes;
 
 	/*
 	 * Allocate memory to store results, and fill with InvalidVXID.  We only
@@ -2890,19 +2923,25 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock. */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getlockmask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getlockmask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2994,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getlockmask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +3014,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3048,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getlockmask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3061,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3076,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 590c026b5b..be09f66532 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,11 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
+extern LOCKMASK GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode);
 extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
 											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMASK getlockmask, int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

#16Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#15)
Re: Exposing the lock manager's WaitForLockers() to SQL

I meant to add that the example in the doc is adapted from Marco
Slot's blog post linked earlier:
https://www.citusdata.com/blog/2018/06/14/scalable-incremental-data-aggregation/

#17Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#16)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Simplified the code and docs, and rewrote the example with more prose
instead of PL/pgSQL, which unfortunately made it longer, although it
could be truncated. Not really sure what's best...

Attachments:

v5-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v5-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 3550c34a20f01973edbe8515be413223e76be282 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v5 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

GetLockers() supports getting lockers in modes matching an arbitrary
lockmask, not just those that conflict with a specific lockmode.
GetLockConflicts() is still available as a convenience wrapper, and its
semantics are unchanged.

Also factor out and export GetLockConflictMask() for use in a later
commit.
---
 src/backend/storage/lmgr/lock.c | 91 +++++++++++++++++++++++----------
 src/include/storage/lock.h      |  3 ++
 2 files changed, 67 insertions(+), 27 deletions(-)

diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c70a1adb9a..41b35de019 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2832,45 +2832,78 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 	return proclock;
 }
 
+/*
+ * GetLockConflictMask
+ *		Return the LOCKMASK of lockmodes that would conflict with the given
+ *		lockmode if taken on locktag.
+ */
+LOCKMASK
+GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode)
+{
+	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
+	LockMethod	lockMethodTable;
+
+	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
+		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
+	lockMethodTable = LockMethods[lockmethodid];
+	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	return lockMethodTable->conflictTab[lockmode];
+}
+
 /*
  * GetLockConflicts
+ *
+ * Convenience wrapper for GetLockers conflicting with a single lockmode.
+ */
+VirtualTransactionId *
+GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+{
+	return GetLockers(locktag, GetLockConflictMask(locktag, lockmode), countp);
+}
+
+/*
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag and matching the specified getMask, which is
+ *		assumed to be valid for locktag. xacts merely awaiting such a lock are
+ *		NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMASK getMask, int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
-		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	numLockModes = lockMethodTable->numLockModes;
 
 	/*
 	 * Allocate memory to store results, and fill with InvalidVXID.  We only
@@ -2890,19 +2923,25 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock. */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2994,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +3014,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3048,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3061,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3076,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 00679624f7..62c50597a8 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,11 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
+extern LOCKMASK GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode);
 extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
 											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMASK getMask, int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v5-0003-Add-WAIT-FOR-LOCKERS-command.patchapplication/octet-stream; name=v5-0003-Add-WAIT-FOR-LOCKERS-command.patchDownload
From 9d5a9b44b29fcbdf427fda7221d068e20dedf60b Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Sat, 23 Dec 2023 01:42:57 -0800
Subject: [PATCH v5 3/3] Add WAIT FOR LOCKERS command

Rather than actually taking any locks on the table(s), it simply waits
for existing lockers using the existing WaitForLockersMultiple()
function in the lock manager.

Currently it's not supported with views, since they would require more
locking to gather the locktags.

See docs and tests for more detail.
---
 doc/src/sgml/ref/allfiles.sgml                |   1 +
 doc/src/sgml/ref/lock.sgml                    |   8 +
 doc/src/sgml/ref/wait_for_lockers.sgml        | 305 ++++++++++++++++++
 doc/src/sgml/reference.sgml                   |   1 +
 src/backend/commands/lockcmds.c               |  63 ++++
 src/backend/parser/gram.y                     |  51 ++-
 src/backend/tcop/utility.c                    |  18 ++
 src/include/commands/lockcmds.h               |   5 +
 src/include/nodes/parsenodes.h                |  12 +
 src/include/parser/kwlist.h                   |   2 +
 src/include/tcop/cmdtaglist.h                 |   1 +
 .../expected/deadlock-wait-for-lockers.out    |  12 +
 .../isolation/expected/wait-for-lockers.out   | 144 +++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/deadlock-wait-for-lockers.spec      |  23 ++
 .../isolation/specs/wait-for-lockers.spec     |  76 +++++
 .../regress/expected/wait_for_lockers.out     |  92 ++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  99 ++++++
 19 files changed, 912 insertions(+), 5 deletions(-)
 create mode 100644 doc/src/sgml/ref/wait_for_lockers.sgml
 create mode 100644 src/test/isolation/expected/deadlock-wait-for-lockers.out
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-for-lockers.spec
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index fda4690eab..f40be2fd0e 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -188,6 +188,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY update             SYSTEM "update.sgml">
 <!ENTITY vacuum             SYSTEM "vacuum.sgml">
 <!ENTITY values             SYSTEM "values.sgml">
+<!ENTITY waitForLockers     SYSTEM "wait_for_lockers.sgml">
 
 <!-- applications and utilities -->
 <!ENTITY clusterdb          SYSTEM "clusterdb.sgml">
diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
index 6ce2518de7..1893f94619 100644
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -268,4 +268,12 @@ COMMIT WORK;
    present in <productname>Oracle</productname>.
   </para>
  </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-waitforlockers"/></member>
+  </simplelist>
+ </refsect1>
 </refentry>
diff --git a/doc/src/sgml/ref/wait_for_lockers.sgml b/doc/src/sgml/ref/wait_for_lockers.sgml
new file mode 100644
index 0000000000..f563984c87
--- /dev/null
+++ b/doc/src/sgml/ref/wait_for_lockers.sgml
@@ -0,0 +1,305 @@
+<!--
+doc/src/sgml/ref/wait_for_lockers.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-waitforlockers">
+ <indexterm zone="sql-waitforlockers">
+  <primary>WAIT FOR LOCKERS</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>WAIT FOR LOCKERS</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>WAIT FOR LOCKERS</refname>
+  <refpurpose>wait for table locks to be released</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+WAIT FOR LOCKERS OF [ TABLE ] [ ONLY ] <replaceable class="parameter">name</replaceable> [ * ] [, ...] [ IN [ CONFLICT WITH ] <replaceable class="parameter">lockmode</replaceable> MODE ]
+
+<phrase>where <replaceable class="parameter">lockmode</replaceable> is one of:</phrase>
+
+    ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE
+    | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> waits for already-held table-level locks
+   to be released, and does not take any new table-level locks.
+  </para>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> first builds a set of transactions that
+   hold matching locks, and then waits for the transactions in the set to
+   release those locks. The set does not include any transaction that is only
+   waiting to take a matching lock but does not yet hold one, nor any
+   transaction that only takes a matching lock after
+   <command>WAIT FOR LOCKERS</command> finishes building the set. The set may or
+   may not include a transaction that only takes a matching lock while
+   <command>WAIT FOR LOCKERS</command> is building the set. The set never
+   includes the transaction that is building the set, even if it holds a
+   matching lock, because that would trivially deadlock.
+  </para>
+
+  <para>
+   <command>WAIT FOR LOCKERS</command> can be used either inside or outside a
+   transaction block. Within a transaction at the
+   <literal>REPEATABLE READ</literal> or <literal>SERIALIZABLE</literal>
+   isolation level, in order to observe any changes made by the waited-for
+   lockers, you have to execute the <command>WAIT FOR LOCKERS</command>
+   statement before executing any <command>SELECT</command> or data modification
+   statement.  A <literal>REPEATABLE READ</literal> or
+   <literal>SERIALIZABLE</literal> transaction's view of data will be frozen
+   when its first <command>SELECT</command> or data modification statement
+   begins.  A <command>WAIT FOR LOCKERS</command> later in the transaction will
+   still wait for outstanding writes &mdash; but it won't ensure that what the
+   transaction reads afterward corresponds to the latest committed values. More
+   information about transaction isolation levels can be found in
+   <xref linkend="transaction-iso"/>.
+  </para>
+
+  <para>
+   Since <command>WAIT FOR LOCKERS</command> does not take any table-level
+   locks, a table may be dropped by another transaction while waiting for
+   lockers of it. Once the drop commits, there are no more lockers of the table
+   to wait for.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing table, whose lockers
+      are waited for. If <literal>ONLY</literal> is specified before the table
+      name, only lockers of that table are waited for. If
+      <literal>ONLY</literal> is not specified, lockers of the table and all its
+      descendant tables (if any) are waited for.  Optionally,
+      <literal>*</literal> can be specified after the table name to explicitly
+      indicate that descendant tables are included.
+     </para>
+
+     <para>
+      When multiple tables are specified and/or descendant tables are included
+      (either explicitly or implicitly), <command>WAIT FOR LOCKERS</command>
+      builds a single combined set of transactions that hold matching locks on
+      any of the tables, and then waits for the transactions in this combined
+      set to release those locks. This may produce a shorter wait than the
+      one-table-at-a-time approach used by <xref linkend="sql-lock"/>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IN</literal></term>
+    <term><literal>IN CONFLICT WITH</literal></term>
+    <listitem>
+     <para>
+      Specifies whether to wait for locks in the following
+      <replaceable class="parameter">lockmode</replaceable>, or locks in modes
+      that conflict with <replaceable class="parameter">lockmode</replaceable>.
+      Note that a lock mode may or may not conflict with itself.  More
+      information about the lock modes and how they conflict can be found in
+      <xref linkend="explicit-locking"/>.
+     </para>
+
+     <para>
+      If this clause is omitted, then
+      <literal>IN CONFLICT WITH ACCESS EXCLUSIVE MODE</literal>, which waits for
+      locks in all lock modes, is used.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">lockmode</replaceable></term>
+    <listitem>
+     <para>
+      The specified lock mode, in conjunction with the previous parameter,
+      determines which lock modes to wait for.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   To wait for locks on a table, the user must have <literal>SELECT</literal>
+   privileges on the table.
+  </para>
+
+  <para>
+   Views are not currently supported.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Example</title>
+
+  <para>
+   <literal>IN ROW EXCLUSIVE MODE</literal> or
+   <literal>IN CONFLICT WITH SHARE MODE</literal> can be used to wait for
+   in-progress writes to be committed or rolled back, while still allowing other
+   transactions to newly acquire conflicting locks and perform writes.  This can
+   be useful in conjunction with non-transactional communication about
+   in-progress writes, such as through sequences.
+  </para>
+
+  <para>
+   For example, given the following table:
+
+<programlisting>
+CREATE TABLE page_views (
+    id bigserial,
+    view_time timestamptz
+);
+</programlisting>
+  </para>
+
+  <para>
+   Assume that this table is never modified except by
+   <xref linkend="sql-insert"/> commands that assign the default value to the
+   <structfield>id</structfield> column. Each transaction executing such an
+   <command>INSERT</command> command will first take a
+   <literal>ROW EXCLUSIVE</literal> lock on <structname>page_views</structname>
+   and then obtain new values for <structfield>id</structfield> from the
+   automatically-created uncached sequence <literal>page_views_id_seq</literal>.
+  </para>
+
+  <para>
+   To observe the progress of insertions, we first use the
+   <literal>pg_sequence_last_value</literal> function to obtain the last (and
+   thus highest) value of <structfield>id</structfield> used by an
+   <command>INSERT</command> command so far:
+
+<programlisting>
+SELECT pg_sequence_last_value('page_views_id_seq');
+
+ pg_sequence_last_value
+------------------------
+                      4
+</programlisting>
+  </para>
+
+  <para>
+   Since some of the transactions that used <structfield>id</structfield> values
+   less than or equal to 4 may not have committed or rolled back yet, we wait
+   for them:
+
+<programlisting>
+WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+</programlisting>
+  </para>
+
+  <para>
+   After <literal>WAIT FOR LOCKERS</literal> returns, we know that all rows
+   where <literal>id &lt;= 4</literal> have been committed or rolled back. Any
+   rows that are committed after this must have <literal>id &gt; 4</literal>.
+   Then we execute:
+
+<programlisting>
+SELECT FROM page_views WHERE id &lt;= 4;
+
+ id |           view_time
+----+-------------------------------
+  2 | 2024-01-01 12:34:01.000000-00
+  3 | 2024-01-01 12:34:00.000000-00
+</programlisting>
+  </para>
+
+  <para>
+   We know that, going forward, these two rows are the only rows with
+   <literal>id &lt;= 4</literal> that can ever exist. (We might also conclude
+   that some transaction(s) tried to insert rows with
+   <structfield>id</structfield> values of 1 and 4, but if so, they must have
+   rolled back.)
+  </para>
+
+  <para>
+   For this to work, the second <command>SELECT</command> command must see a
+   snapshot of the database taken after <literal>WAIT FOR LOCKERS</literal>
+   returned. For example, it cannot be in the same
+   <literal>REPEATABLE READ</literal> or <literal>SERIALIZABLE</literal>
+   transaction as the first <command>SELECT</command> command, because that
+   would have taken the transaction's snapshot too early.
+  </para>
+
+  <para>
+   Note that some rows with <literal>id &gt; 4</literal> might have been
+   committed already, such as while we were executing
+   <literal>WAIT FOR LOCKERS</literal>, and of course more may be committed in
+   the future.
+  </para>
+
+  <para>
+   We can continue to observe new rows by iterating again:
+
+<programlisting>
+SELECT pg_sequence_last_value('page_views_id_seq');
+
+ pg_sequence_last_value
+------------------------
+                      9
+</programlisting>
+
+<programlisting>
+WAIT FOR LOCKERS OF page_views IN ROW EXCLUSIVE MODE;
+</programlisting>
+  </para>
+
+  <para>
+   We already observed all of the rows where <literal>id &lt;= 4</literal>, so
+   this time we can filter them out:
+
+<programlisting>
+SELECT FROM page_views WHERE id &gt; 4 AND id &lt;= 9;
+
+ id |           view_time
+----+-------------------------------
+  5 | 2024-01-01 12:34:05.000000-00
+  8 | 2024-01-01 12:34:04.000000-00
+  9 | 2024-01-01 12:34:07.000000-00
+</programlisting>
+  </para>
+
+  <para>
+   These three rows and the two rows we observed above are the only rows with
+   <literal>id &lt;= 9</literal> that can ever exist going forward. We can
+   continue iterating like this to incrementally observe more newly inserted
+   rows.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>WAIT FOR LOCKERS</command> in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-lock"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a07d2b5e01..cd827a12ca 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -216,6 +216,7 @@
    &update;
    &vacuum;
    &values;
+   &waitForLockers;
 
  </reference>
 
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 29e9953bf4..c15db21ea7 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -16,6 +16,7 @@
 
 #include "access/table.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_inherits.h"
 #include "commands/lockcmds.h"
@@ -29,6 +30,7 @@
 #include "utils/syscache.h"
 
 static void LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait);
+static LOCKTAG *GetLocktag(Oid reloid);
 static AclResult LockTableAclCheck(Oid reloid, LOCKMODE lockmode, Oid userid);
 static void RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid,
 										 Oid oldrelid, void *arg);
@@ -64,6 +66,49 @@ LockTableCommand(LockStmt *lockstmt)
 	}
 }
 
+/*
+ * WAIT FOR LOCKERS
+ */
+void
+WaitForLockersCommand(WaitForLockersStmt *waitstmt)
+{
+	ListCell   *p;
+	List	   *locktags = NIL;
+
+	/*
+	 * Iterate over the list and process the named relations one at a time
+	 */
+	foreach(p, waitstmt->relations)
+	{
+		RangeVar   *rv = (RangeVar *) lfirst(p);
+		bool		recurse = rv->inh;
+		LOCKMODE	nolock = NoLock;
+		Oid			reloid;
+
+		reloid = RangeVarGetRelidExtended(rv, NoLock, 0,
+										  RangeVarCallbackForLockTable,
+										  (void *) &nolock);
+		if (get_rel_relkind(reloid) == RELKIND_VIEW)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("WAIT FOR LOCKERS is not supported with views")));
+
+		if (recurse)
+		{
+			List	   *children;
+
+			children = find_all_inheritors(reloid, NoLock, NULL);
+
+			foreach_oid(childreloid, children)
+				locktags = lappend(locktags, GetLocktag(childreloid));
+		}
+		else
+			locktags = lappend(locktags, GetLocktag(reloid));
+	}
+	WaitForLockersMultiple(locktags, waitstmt->mode, waitstmt->conflicting,
+						   false);
+}
+
 /*
  * Before acquiring a table lock on the named table, check whether we have
  * permission to do so.
@@ -158,6 +203,24 @@ LockTableRecurse(Oid reloid, LOCKMODE lockmode, bool nowait)
 	}
 }
 
+/*
+ * Get locktag for a single rel
+ */
+static LOCKTAG *
+GetLocktag(Oid reloid)
+{
+	LOCKTAG	   *heaplocktag;
+	Oid			dbid;
+
+	heaplocktag = palloc_object(LOCKTAG);
+	if (IsSharedRelation(reloid))
+		dbid = InvalidOid;
+	else
+		dbid = MyDatabaseId;
+	SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+	return heaplocktag;
+}
+
 /*
  * Apply LOCK TABLE recursively over a view
  *
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6b88096e8e..ce14a1f1b3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -319,6 +319,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateMatViewStmt RefreshMatViewStmt CreateAmStmt
 		CreatePublicationStmt AlterPublicationStmt
 		CreateSubscriptionStmt AlterSubscriptionStmt DropSubscriptionStmt
+		WaitForLockersStmt
 
 %type <node>	select_no_parens select_with_parens select_clause
 				simple_select values_clause
@@ -345,7 +346,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				transaction_mode_item
 				create_extension_opt_item alter_extension_opt_item
 
-%type <ival>	opt_lock lock_type cast_context
+%type <ival>	opt_lock lock_type opt_wait_lock_modes cast_context
 %type <str>		utility_option_name
 %type <defelt>	utility_option_elem
 %type <list>	utility_option_list
@@ -353,7 +354,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <defelt>	drop_option
 %type <boolean>	opt_or_replace opt_no
 				opt_grant_grant_option
-				opt_nowait opt_if_exists opt_with_data
+				opt_nowait opt_conflict_with opt_if_exists opt_with_data
 				opt_transaction_chain
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
@@ -729,7 +730,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
-	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
+	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOCKERS LOGGED
 
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
@@ -773,7 +774,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -1102,6 +1103,7 @@ stmt:
 			| VariableSetStmt
 			| VariableShowStmt
 			| ViewStmt
+			| WaitForLockersStmt
 			| /*EMPTY*/
 				{ $$ = NULL; }
 		;
@@ -12267,6 +12269,43 @@ opt_nowait_or_skip:
 		;
 
 
+/*****************************************************************************
+ *
+ *		QUERY:
+ *				WAIT FOR LOCKERS
+ *
+ *****************************************************************************/
+
+WaitForLockersStmt:
+			WAIT FOR LOCKERS OF opt_table relation_expr_list opt_wait_lock_modes
+				{
+					WaitForLockersStmt *n = makeNode(WaitForLockersStmt);
+
+					n->relations = $6;
+					/* XXX: see opt_wait_lock_modes */
+					n->mode = $7 & ((1 << MaxLockMode) - 1);
+					n->conflicting = ($7 >> MaxLockMode) != 0;
+					$$ = (Node *) n;
+				}
+		;
+
+opt_wait_lock_modes:
+			/*
+			 * XXX: hackily store a bool and a lock mode in an int; should
+			 * probably make a new Node type?
+			 */
+			IN_P opt_conflict_with lock_type MODE
+					{ $$ = ((int)$2 << MaxLockMode) | $3; }
+			| /*EMPTY*/
+					{ $$ = (1 << MaxLockMode) | AccessExclusiveLock; }
+		;
+
+opt_conflict_with:
+			CONFLICT WITH					{ $$ = true; }
+			| /*EMPTY*/						{ $$ = false; }
+		;
+
+
 /*****************************************************************************
  *
  *		QUERY:
@@ -17265,6 +17304,7 @@ unreserved_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -17424,6 +17464,7 @@ unreserved_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHITESPACE_P
 			| WITHIN
 			| WITHOUT
@@ -17855,6 +17896,7 @@ bare_label_keyword:
 			| LOCATION
 			| LOCK_P
 			| LOCKED
+			| LOCKERS
 			| LOGGED
 			| MAPPING
 			| MATCH
@@ -18055,6 +18097,7 @@ bare_label_keyword:
 			| VIEW
 			| VIEWS
 			| VOLATILE
+			| WAIT
 			| WHEN
 			| WHITESPACE_P
 			| WORK
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 8de821f960..053c576440 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -360,6 +360,11 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 					return COMMAND_IS_STRICTLY_READ_ONLY;
 			}
 
+		case T_WaitForLockersStmt:
+			{
+				return COMMAND_IS_STRICTLY_READ_ONLY;
+			}
+
 		case T_TransactionStmt:
 			{
 				TransactionStmt *stmt = (TransactionStmt *) parsetree;
@@ -941,6 +946,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 			LockTableCommand((LockStmt *) parsetree);
 			break;
 
+		case T_WaitForLockersStmt:
+
+			WaitForLockersCommand((WaitForLockersStmt *) parsetree);
+			break;
+
 		case T_ConstraintsSetStmt:
 			WarnNoTransactionBlock(isTopLevel, "SET CONSTRAINTS");
 			AfterTriggerSetState((ConstraintsSetStmt *) parsetree);
@@ -2993,6 +3003,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_LOCK_TABLE;
 			break;
 
+		case T_WaitForLockersStmt:
+			tag = CMDTAG_WAIT_FOR_LOCKERS;
+			break;
+
 		case T_ConstraintsSetStmt:
 			tag = CMDTAG_SET_CONSTRAINTS;
 			break;
@@ -3614,6 +3628,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;
 			break;
 
+		case T_WaitForLockersStmt:
+			lev = LOGSTMT_ALL;
+			break;
+
 		case T_ConstraintsSetStmt:
 			lev = LOGSTMT_ALL;
 			break;
diff --git a/src/include/commands/lockcmds.h b/src/include/commands/lockcmds.h
index c3b2839f3f..53a5e662fc 100644
--- a/src/include/commands/lockcmds.h
+++ b/src/include/commands/lockcmds.h
@@ -21,4 +21,9 @@
  */
 extern void LockTableCommand(LockStmt *lockstmt);
 
+/*
+ * WAIT FOR LOCKERS
+ */
+extern void WaitForLockersCommand(WaitForLockersStmt *lockstmt);
+
 #endif							/* LOCKCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b3181f34ae..9ae94cf46c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3798,6 +3798,18 @@ typedef struct LockStmt
 	bool		nowait;			/* no wait mode */
 } LockStmt;
 
+/* ----------------------
+ *		WAIT FOR LOCKERS Statement
+ * ----------------------
+ */
+typedef struct WaitForLockersStmt
+{
+	NodeTag		type;
+	List	   *relations;		/* relations to wait for */
+	int			mode;			/* lock mode */
+	bool		conflicting;	/* wait for locks conflicting with mode? */
+} WaitForLockersStmt;
+
 /* ----------------------
  *		SET CONSTRAINTS Statement
  * ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 2331acac09..019e66d3e8 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -259,6 +259,7 @@ PG_KEYWORD("localtimestamp", LOCALTIMESTAMP, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("location", LOCATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("lockers", LOCKERS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -473,6 +474,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 7fdcec6dd9..f0925a98db 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -217,3 +217,4 @@ PG_CMDTAG(CMDTAG_TRUNCATE_TABLE, "TRUNCATE TABLE", false, false, false)
 PG_CMDTAG(CMDTAG_UNLISTEN, "UNLISTEN", false, false, false)
 PG_CMDTAG(CMDTAG_UPDATE, "UPDATE", false, false, true)
 PG_CMDTAG(CMDTAG_VACUUM, "VACUUM", false, false, false)
+PG_CMDTAG(CMDTAG_WAIT_FOR_LOCKERS, "WAIT FOR LOCKERS", false, false, false)
diff --git a/src/test/isolation/expected/deadlock-wait-for-lockers.out b/src/test/isolation/expected/deadlock-wait-for-lockers.out
new file mode 100644
index 0000000000..2241a7e999
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-for-lockers.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1lre s2las s2wfl s1wfl s1c s2c
+step s1lre: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2las: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step s1wfl: WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; <waiting ...>
+step s1wfl: <... completed>
+step s2wfl: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..55e556d964
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,144 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflic r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflic w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflic w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflic w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflic w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflic: WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflic: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..b7380627d7 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-for-lockers
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-for-lockers.spec b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
new file mode 100644
index 0000000000..1b34e3cb9b
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
@@ -0,0 +1,23 @@
+setup
+{
+	CREATE TABLE a1 ();
+}
+
+teardown
+{
+	DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1lre	{ LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE; }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2las	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2wfl	{ WAIT FOR LOCKERS OF TABLE a1 IN CONFLICT WITH SHARE MODE; }
+step s2c	{ COMMIT; }
+
+permutation s1lre s2las s2wfl s1wfl s1c s2c
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..afdb6afcb8
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,76 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ WAIT FOR LOCKERS OF TABLE t1, t2 IN ROW EXCLUSIVE MODE; }
+step r_wflic	{ WAIT FOR LOCKERS OF TABLE t1, t2 IN CONFLICT WITH SHARE MODE; }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of WAIT FOR LOCKERS:
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflic r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflic w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between WAIT FOR LOCKERS and nearest equivalent LOCK + ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflic w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock, allows writer1 to then take an ACCESS EXCLUSIVE lock on t2 and
+# another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's later
+# locks...
+permutation w2_in1 r_wflic w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflic w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to
+# release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..91d3502267
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,92 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ERROR:  permission denied for table wfl_tbl2
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..d2ec0a6a86 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..695ac32de0
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,99 @@
+--
+-- Test the WAIT FOR LOCKERS statement
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW SHARE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE UPDATE EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH SHARE MODE ;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH SHARE ROW EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS is allowed outside a transaction
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+
+-- Verify that we can wait for a table with inheritance children.
+CREATE TABLE wfl_tbl2 (b BIGINT) INHERITS (wfl_tbl1);
+CREATE TABLE wfl_tbl3 () INHERITS (wfl_tbl2);
+BEGIN TRANSACTION;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 * IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+ROLLBACK;
+
+-- WAIT FOR LOCKERS requires SELECT permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS SHARE MODE;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS SHARE MODE;
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1 IN CONFLICT WITH ACCESS EXCLUSIVE MODE;
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- Child tables can be waited on without granting explicit permission to do so
+-- as long as we have permission to lock the parent.
+GRANT UPDATE ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+SET ROLE regress_rol_wfl1;
+-- fail when child waited for directly
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl2;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE wfl_tbl1 *;
+ROLLBACK;
+BEGIN;
+WAIT FOR LOCKERS OF TABLE ONLY wfl_tbl1;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl3;
+DROP TABLE wfl_tbl2;
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

v5-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v5-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From 7ef4dc6f02f94e72a05edb3f956cc205ee5431f9 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v5 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lockmode, rather than all lockmodes
that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 29 ++++++++++++++++-------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 88f7994b5a..3706e2df46 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2290,7 +2290,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2306,7 +2306,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 340248a3f2..bf518a2719 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1660,7 +1660,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1707,7 +1707,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -3924,7 +3924,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -3983,7 +3983,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4141,7 +4141,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4175,7 +4175,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2822b2bb44..f2d510522e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19444,7 +19444,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 4975d4b67d..58dd6f256e 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -920,11 +921,13 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 	foreach(lc, locktags)
 	{
 		LOCKTAG    *locktag = lfirst(lc);
+		LOCKMASK	lockmask = conflicting ?
+			GetLockConflictMask(locktag, lockmode) : LOCKBIT_ON(lockmode);
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmask,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -933,8 +936,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +986,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

#18Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Will Mortensen (#17)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Sat, 2024-01-06 at 02:57 -0800, Will Mortensen wrote:

Simplified the code and docs, and rewrote the example with more prose
instead of PL/pgSQL, which unfortunately made it longer, although it
could be truncated. Not really sure what's best...

I thought about this idea, and I have some doubts.

WAIT FOR LOCKERS only waits for transactions that were holding locks
when the statement started. Transactions that obtailed locks later on
are ignored. While your original use case is valid, I cannot think of
any other use case. So it is a special-purpose statement that is only
useful for certain processing of append-only tables.

Is it worth creating a new SQL statement for that, which could lead to
a conflict with future editions of the SQL standard? Couldn't we follow
the PostgreSQL idiosyncrasy of providing a function with side effects
instead?

Yours,
Laurenz Albe

#19Will Mortensen
will@extrahop.com
In reply to: Laurenz Albe (#18)
Re: Exposing the lock manager's WaitForLockers() to SQL

Hi Laurenz, thanks for taking a look!

On Sat, Jan 6, 2024 at 4:00 AM Laurenz Albe <laurenz.albe@cybertec.at> wrote:

While your original use case is valid, I cannot think of
any other use case. So it is a special-purpose statement that is only
useful for certain processing of append-only tables.

It is definitely somewhat niche. :-) But as I mentioned in my
longwinded original message, the scheme is easily extended (with some
tradeoffs) to process updates, if they set a non-primary-key column
using a sequence. As for deletions though, our applications handle
them separately.

Is it worth creating a new SQL statement for that, which could lead to
a conflict with future editions of the SQL standard? Couldn't we follow
the PostgreSQL idiosyncrasy of providing a function with side effects
instead?

I would be happy to add a pg_foo() function instead. Here are a few
things to figure out:

* To support waiting for lockers in a specified mode vs. conflicting
with a specified mode, should there be two functions, or one function
with a boolean argument like I used in C?

* Presumably the function(s) would take a regclass[] argument?

* Presumably the lock mode would be specified using strings like
'ShareLock'? There's no code to parse these AFAICT, but we could add
it.

* Maybe we could omit LOCK's handling of descendant tables for
simplicity? I will have to see how much other code needs to be
duplicated or shared.

I'll look further into it later this week.

#20Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#19)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Here is a new series adding a single pg_wait_for_lockers() function
that takes a boolean argument to control the interpretation of the
lock mode. It omits LOCK's handling of descendant tables so it
requires permissions directly on descendants in order to wait for
locks on them. Not sure if that would be a problem for anyone.

Attachments:

v6-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v6-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 4451fbaef21046fddc5aaa64e2819a8bb0e48dd1 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v6 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

GetLockers() supports getting lockers in modes matching an arbitrary
lockmask, not just those that conflict with a specific lockmode.
GetLockConflicts() is still available as a convenience wrapper, and its
semantics are unchanged.

Also factor out and export GetLockConflictMask() for use in a later
commit.
---
 src/backend/storage/lmgr/lock.c | 91 +++++++++++++++++++++++----------
 src/include/storage/lock.h      |  3 ++
 2 files changed, 67 insertions(+), 27 deletions(-)

diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c70a1adb9a..41b35de019 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2832,45 +2832,78 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 	return proclock;
 }
 
+/*
+ * GetLockConflictMask
+ *		Return the LOCKMASK of lockmodes that would conflict with the given
+ *		lockmode if taken on locktag.
+ */
+LOCKMASK
+GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode)
+{
+	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
+	LockMethod	lockMethodTable;
+
+	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
+		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
+	lockMethodTable = LockMethods[lockmethodid];
+	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	return lockMethodTable->conflictTab[lockmode];
+}
+
 /*
  * GetLockConflicts
+ *
+ * Convenience wrapper for GetLockers conflicting with a single lockmode.
+ */
+VirtualTransactionId *
+GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+{
+	return GetLockers(locktag, GetLockConflictMask(locktag, lockmode), countp);
+}
+
+/*
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag and matching the specified getMask, which is
+ *		assumed to be valid for locktag. xacts merely awaiting such a lock are
+ *		NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMASK getMask, int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
-		elog(ERROR, "unrecognized lock mode: %d", lockmode);
+	numLockModes = lockMethodTable->numLockModes;
 
 	/*
 	 * Allocate memory to store results, and fill with InvalidVXID.  We only
@@ -2890,19 +2923,25 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock. */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2994,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +3014,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3048,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3061,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3076,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 00679624f7..62c50597a8 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,11 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
+extern LOCKMASK GetLockConflictMask(const LOCKTAG *locktag, LOCKMODE lockmode);
 extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
 											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMASK getMask, int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v6-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v6-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From 3ad0eea74302f3660c53449ce88dcc041f9c5bbd Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v6 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lockmode, rather than all lockmodes
that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 29 ++++++++++++++++-------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 88f7994b5a..3706e2df46 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2290,7 +2290,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2306,7 +2306,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 340248a3f2..bf518a2719 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1660,7 +1660,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1707,7 +1707,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -3924,7 +3924,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -3983,7 +3983,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4141,7 +4141,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4175,7 +4175,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2822b2bb44..f2d510522e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19444,7 +19444,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 4975d4b67d..58dd6f256e 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -920,11 +921,13 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 	foreach(lc, locktags)
 	{
 		LOCKTAG    *locktag = lfirst(lc);
+		LOCKMASK	lockmask = conflicting ?
+			GetLockConflictMask(locktag, lockmode) : LOCKBIT_ON(lockmode);
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmask,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -933,8 +936,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +986,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v6-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v6-0003-Add-pg_wait_for_lockers-function.patchDownload
From 87592b2189e330a0095f394b870e9e6f534763e5 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v6 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for existing lockers using the existing WaitForLockersMultiple()
function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  80 ++++++++
 src/backend/storage/lmgr/lock.c               |  21 ++
 src/backend/utils/adt/lockfuncs.c             | 100 ++++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../expected/deadlock-wait-for-lockers.out    |  19 ++
 .../isolation/expected/wait-for-lockers.out   | 180 +++++++++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/deadlock-wait-for-lockers.spec      |  25 +++
 .../isolation/specs/wait-for-lockers.spec     |  78 ++++++++
 .../regress/expected/wait_for_lockers.out     | 185 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  84 ++++++++
 13 files changed, 782 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-for-lockers.out
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-for-lockers.spec
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index de78d58d4b..06a2286bbe 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28928,6 +28928,86 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        If <parameter>conflicting</parameter> is true, the function waits for
+        locks in modes that conflict with <parameter>lockmode</parameter>;
+        otherwise it waits for locks in <parameter>lockmode</parameter>. Note
+        that a lock mode may or may not conflict with itself.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>ShareLock</literal>.
+       </para>
+       <para>
+        The function first builds a combined set of transactions that hold
+        matching locks on any of the tables, and then waits for the transactions
+        in the set to release those locks. The set does not include any
+        transaction that is only waiting to take a matching lock but does not
+        yet hold one, nor any transaction that only takes a matching lock after
+        the function finishes building the set. The set may or may not include a
+        transaction that only takes a matching lock while the function is
+        building the set. The set never includes the transaction that is
+        building the set, even if it holds a matching lock, because that would
+        trivially deadlock.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 41b35de019..cb54e39944 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4062,6 +4062,27 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 4b49f7fe3d..beec595e6b 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -14,12 +14,16 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +605,102 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are primarily just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * inheritance/partition relationships.
+ *
+ * Since no locks are taken, there are some inherent races. The specified tables
+ * must exist long enough for us to check their permissions, but could be
+ * dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS) /*, bool conflicting)*/
+{
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+	List	   *locktags = NIL;
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		/*
+		 * Require the oid to refer to an existing table (at least transiently)
+		 * so the caller can be confident that they supplied a valid argument
+		 * and actually waited for something (even if the table ends up being
+		 * dropped while they wait)
+		 */
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* for error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		/* Currently, we only allow plain tables */
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7979392776..d36bf301d9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 62c50597a8..44101e9d3e 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -593,6 +593,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/deadlock-wait-for-lockers.out b/src/test/isolation/expected/deadlock-wait-for-lockers.out
new file mode 100644
index 0000000000..8447ff6f05
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-for-lockers.out
@@ -0,0 +1,19 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1lre s2las s2wfl s1wfl s1c s2c
+step s1lre: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2las: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2wfl: SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'ShareLock', TRUE); <waiting ...>
+step s1wfl: SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'AccessExclusiveLock', TRUE); <waiting ...>
+step s1wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step s2wfl: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..a3e892549d
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..b7380627d7 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-for-lockers
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-for-lockers.spec b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
new file mode 100644
index 0000000000..47f07004ae
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
@@ -0,0 +1,25 @@
+setup
+{
+	CREATE TABLE a1 ();
+}
+
+teardown
+{
+	DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1lre	{ LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1wfl	{ SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'AccessExclusiveLock', TRUE); }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2las	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2wfl	{ SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'ShareLock', TRUE); }
+step s2c	{ COMMIT; }
+
+permutation s1lre s2las s2wfl s1wfl s1c s2c
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..84f203bd57
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,78 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of WAIT FOR LOCKERS:
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflcting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between WAIT FOR LOCKERS and nearest equivalent LOCK + ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflcting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock, allows writer1 to then take an ACCESS EXCLUSIVE lock on t2 and
+# another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's later
+# locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflcting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflcting lock also waits for writer2 to
+# release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..d7dc9c5ac8
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,185 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..d2ec0a6a86 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..c3b2ff1395
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,84 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options; also try omitting the optional TABLE keyword.
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

#21vignesh C
vignesh21@gmail.com
In reply to: Will Mortensen (#20)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Thu, 11 Jan 2024 at 15:22, Will Mortensen <will@extrahop.com> wrote:

Here is a new series adding a single pg_wait_for_lockers() function
that takes a boolean argument to control the interpretation of the
lock mode. It omits LOCK's handling of descendant tables so it
requires permissions directly on descendants in order to wait for
locks on them. Not sure if that would be a problem for anyone.

CFBot shows that there is one warning as in [1]https://api.cirrus-ci.com/v1/task/4884224944111616/logs/build.log:
patching file doc/src/sgml/libpq.sgml
...
[09:30:40.000] [940/2212] Compiling C object
src/backend/postgres_lib.a.p/storage_lmgr_condition_variable.c.obj
[09:30:40.000] [941/2212] Compiling C object
src/backend/postgres_lib.a.p/storage_lmgr_deadlock.c.obj
[09:30:40.000] [942/2212] Compiling C object
src/backend/postgres_lib.a.p/storage_lmgr_lmgr.c.obj
[09:30:40.000] [943/2212] Compiling C object
src/backend/postgres_lib.a.p/storage_lmgr_lock.c.obj
[09:30:40.000] c:\cirrus\src\backend\storage\lmgr\lock.c(4084) :
warning C4715: 'ParseLockmodeName': not all control paths return a
value

Please post an updated version for the same.

[1]: https://api.cirrus-ci.com/v1/task/4884224944111616/logs/build.log

Regards,
Vignesh

#22Will Mortensen
will@extrahop.com
In reply to: vignesh C (#21)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Fri, Jan 26, 2024 at 4:54 AM vignesh C <vignesh21@gmail.com> wrote:

CFBot shows that there is one warning as in [1]:
patching file doc/src/sgml/libpq.sgml
...
[09:30:40.000] [943/2212] Compiling C object
src/backend/postgres_lib.a.p/storage_lmgr_lock.c.obj
[09:30:40.000] c:\cirrus\src\backend\storage\lmgr\lock.c(4084) :
warning C4715: 'ParseLockmodeName': not all control paths return a
value

Thanks Vignesh, I guess the MS compiler doesn't have
__builtin_constant_p()? So I added an unreachable return, and a
regression test that exercises this error path.

I also made various other simplifications and minor fixes to the code,
docs, and tests.

Back in v5 (with a new SQL command) I had a detailed example in the
docs, which I removed when changing to a function, and I'm not sure if
I should try to add it back now...I could shrink it but it might still
be too long for this part of the docs?

Anyway, please see attached.

Attachments:

v7-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v7-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 3a8c57e622bff66b5b0f33a3356e5d69c7b7c1e8 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v7 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

This also supports getting lockers in a single specified lock mode,
rather than all modes that conflict with a specified mode.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  4 +-
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 52 insertions(+), 38 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 8426458f7f..41394c88fa 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -473,7 +473,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->lxid = xid;
 		proc->backendId = InvalidBackendId;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e..7d4be438c2 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2584,8 +2584,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index d8755a106d..d59cb2d348 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -657,7 +657,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -711,7 +711,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 4975d4b67d..a0e3eea3a0 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -923,8 +923,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c70a1adb9a..0542c83f1f 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2833,43 +2833,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2890,19 +2897,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2970,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +2990,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3024,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3037,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3052,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..6490fbb7ee 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1299,8 +1299,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 00679624f7..bf8ca45ff8 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v7-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v7-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From e2b49b591b3d61849f37fd69cef518c617254451 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v7 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lock mode, rather than all lock
modes that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 25 +++++++++++++------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 27 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4b88a9cb87..09c64997c4 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2303,7 +2303,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2319,7 +2319,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 7a87626f5f..c43625375b 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1674,7 +1674,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1721,7 +1721,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -4039,7 +4039,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4098,7 +4098,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4256,7 +4256,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4290,7 +4290,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 68f658e834..72007e13b4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19615,7 +19615,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index a0e3eea3a0..885291bba1 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -923,7 +924,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -933,8 +934,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +984,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v7-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v7-0003-Add-pg_wait_for_lockers-function.patchDownload
From 91b24e1623120d4b04fb4c15f750ecc199b208f5 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v7 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, using the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  82 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  93 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../expected/deadlock-wait-for-lockers.out    |  19 ++
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/deadlock-wait-for-lockers.spec      |  25 +++
 .../isolation/specs/wait-for-lockers.spec     |  79 +++++++
 .../regress/expected/wait_for_lockers.out     | 192 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  90 ++++++++
 13 files changed, 793 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/deadlock-wait-for-lockers.out
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/deadlock-wait-for-lockers.spec
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6788ba8ef4..8e90d0e634 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29380,6 +29380,88 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 0542c83f1f..dc8c4b7a2b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4038,6 +4038,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 4b49f7fe3d..6c08c7ef8b 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -14,12 +14,16 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +605,95 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are primarily just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS) /*, bool conflicting)*/
+{
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+	List	   *locktags = NIL;
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..1347c84d49 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index bf8ca45ff8..f3c77e42f0 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -591,6 +591,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/deadlock-wait-for-lockers.out b/src/test/isolation/expected/deadlock-wait-for-lockers.out
new file mode 100644
index 0000000000..8447ff6f05
--- /dev/null
+++ b/src/test/isolation/expected/deadlock-wait-for-lockers.out
@@ -0,0 +1,19 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1lre s2las s2wfl s1wfl s1c s2c
+step s1lre: LOCK TABLE a1 IN ROW EXCLUSIVE MODE;
+step s2las: LOCK TABLE a1 IN ACCESS SHARE MODE;
+step s2wfl: SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'ShareLock', TRUE); <waiting ...>
+step s1wfl: SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'AccessExclusiveLock', TRUE); <waiting ...>
+step s1wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step s2wfl: <... completed>
+ERROR:  deadlock detected
+step s1c: COMMIT;
+step s2c: COMMIT;
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..a3e892549d
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..b7380627d7 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
@@ -20,6 +21,7 @@ test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
 test: deadlock-simple
+test: deadlock-wait-for-lockers
 test: deadlock-hard
 test: deadlock-soft
 test: deadlock-soft-2
diff --git a/src/test/isolation/specs/deadlock-wait-for-lockers.spec b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
new file mode 100644
index 0000000000..47f07004ae
--- /dev/null
+++ b/src/test/isolation/specs/deadlock-wait-for-lockers.spec
@@ -0,0 +1,25 @@
+setup
+{
+	CREATE TABLE a1 ();
+}
+
+teardown
+{
+	DROP TABLE a1;
+}
+
+session s1
+setup		{ BEGIN; }
+step s1lre	{ LOCK TABLE a1 IN ROW EXCLUSIVE MODE; }
+step s1wfl	{ SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'AccessExclusiveLock', TRUE); }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ BEGIN; }
+step s2las	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
+step s2wfl	{ SELECT pg_wait_for_lockers(array['a1']::regclass[],
+										 'ShareLock', TRUE); }
+step s2c	{ COMMIT; }
+
+permutation s1lre s2las s2wfl s1wfl s1c s2c
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..c02bd884df
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..24415edd7a
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,192 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..43e4493205 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..a299cd7863
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,90 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

#23Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#22)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

I guess the output of the deadlock test was unstable, so I simply
removed it in v8 here, but I can try to fix it instead if it seems
important to test that.

Attachments:

v8-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v8-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 3a8c57e622bff66b5b0f33a3356e5d69c7b7c1e8 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v8 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

This also supports getting lockers in a single specified lock mode,
rather than all modes that conflict with a specified mode.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  4 +-
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 52 insertions(+), 38 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 8426458f7f..41394c88fa 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -473,7 +473,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->lxid = xid;
 		proc->backendId = InvalidBackendId;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e..7d4be438c2 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2584,8 +2584,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index d8755a106d..d59cb2d348 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -657,7 +657,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -711,7 +711,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 4975d4b67d..a0e3eea3a0 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -923,8 +923,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c70a1adb9a..0542c83f1f 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2833,43 +2833,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2890,19 +2897,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2970,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +2990,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3024,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3037,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3052,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..6490fbb7ee 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1299,8 +1299,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 00679624f7..bf8ca45ff8 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v8-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v8-0003-Add-pg_wait_for_lockers-function.patchDownload
From ee8fc3fd31924f02d7ca19d8832fcf11737938d5 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v8 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, using the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  82 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  93 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/wait-for-lockers.spec     |  79 +++++++
 .../regress/expected/wait_for_lockers.out     | 192 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  90 ++++++++
 11 files changed, 748 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6788ba8ef4..8e90d0e634 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29380,6 +29380,88 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 0542c83f1f..dc8c4b7a2b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4038,6 +4038,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 4b49f7fe3d..6c08c7ef8b 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -14,12 +14,16 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +605,95 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are primarily just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS) /*, bool conflicting)*/
+{
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+	List	   *locktags = NIL;
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..1347c84d49 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index bf8ca45ff8..f3c77e42f0 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -591,6 +591,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..a3e892549d
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..dd08f2dfe8 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..c02bd884df
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..24415edd7a
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,192 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..43e4493205 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..a299cd7863
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,90 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

v8-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v8-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From e2b49b591b3d61849f37fd69cef518c617254451 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v8 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lock mode, rather than all lock
modes that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 25 +++++++++++++------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 27 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4b88a9cb87..09c64997c4 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2303,7 +2303,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2319,7 +2319,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 7a87626f5f..c43625375b 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1674,7 +1674,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1721,7 +1721,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -4039,7 +4039,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4098,7 +4098,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4256,7 +4256,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4290,7 +4290,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 68f658e834..72007e13b4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19615,7 +19615,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index a0e3eea3a0..885291bba1 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -923,7 +924,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -933,8 +934,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +984,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

#24Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#23)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Minor style fix; sorry for the spam.

Attachments:

v9-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchapplication/octet-stream; name=v9-0001-Refactor-GetLockConflicts-into-more-general-GetLo.patchDownload
From 3a8c57e622bff66b5b0f33a3356e5d69c7b7c1e8 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v9 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

This also supports getting lockers in a single specified lock mode,
rather than all modes that conflict with a specified mode.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  4 +-
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 52 insertions(+), 38 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 8426458f7f..41394c88fa 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -473,7 +473,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->lxid = xid;
 		proc->backendId = InvalidBackendId;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e..7d4be438c2 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2584,8 +2584,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index d8755a106d..d59cb2d348 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -657,7 +657,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -711,7 +711,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 4975d4b67d..a0e3eea3a0 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -923,8 +923,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c70a1adb9a..0542c83f1f 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2833,43 +2833,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2890,19 +2897,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2955,12 +2970,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2975,7 +2990,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3009,11 +3024,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3022,8 +3037,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3039,7 +3052,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].backendId = InvalidBackendId;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe..6490fbb7ee 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1299,8 +1299,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 00679624f7..bf8ca45ff8 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -574,8 +574,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v9-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchapplication/octet-stream; name=v9-0002-Allow-specifying-single-lockmode-in-WaitForLocker.patchDownload
From e2b49b591b3d61849f37fd69cef518c617254451 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v9 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lock mode, rather than all lock
modes that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 25 +++++++++++++------------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 27 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4b88a9cb87..09c64997c4 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2303,7 +2303,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2319,7 +2319,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 7a87626f5f..c43625375b 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1674,7 +1674,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1721,7 +1721,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -4039,7 +4039,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4098,7 +4098,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4256,7 +4256,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4290,7 +4290,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 68f658e834..72007e13b4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19615,7 +19615,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index a0e3eea3a0..885291bba1 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -893,19 +893,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -923,7 +924,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -933,8 +934,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
@@ -983,16 +984,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v9-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v9-0003-Add-pg_wait_for_lockers-function.patchDownload
From ee2dc518d46f17f4c8dcee735a3614e0919b8b31 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v9 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, using the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  82 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  93 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/wait-for-lockers.spec     |  79 +++++++
 .../regress/expected/wait_for_lockers.out     | 192 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  90 ++++++++
 11 files changed, 748 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6788ba8ef4..8e90d0e634 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29380,6 +29380,88 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 0542c83f1f..dc8c4b7a2b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4038,6 +4038,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 4b49f7fe3d..aa917b44c5 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -14,12 +14,16 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +605,95 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are primarily just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS)
+{
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+	List	   *locktags = NIL;
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 29af4ce65d..1347c84d49 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index bf8ca45ff8..f3c77e42f0 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -591,6 +591,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..a3e892549d
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..dd08f2dfe8 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..c02bd884df
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..24415edd7a
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,192 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..43e4493205 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..a299cd7863
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,90 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

#25Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#24)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Rebased and fixed conflicts.

FWIW re: Andrey's comment in his excellent CF summary email[0]/messages/by-id/C8D65462-0888-4484-A72C-C99A94381ECD@yandex-team.ru: we're
currently using vanilla Postgres (via Gentoo) on single nodes, and not
anything fancy like Citus. The Citus relationship is just that we were
inspired by Marco's blog post there. We have a variety of clients
written in different languages that generally don't coordinate their
table modifications, and Marco's scheme merely requires them to use
sequences idiomatically, which we can just about manage. :-)

This feature is then a performance optimization to support this scheme
while avoiding the case where one writer holding a RowExclusiveLock
blocks the reader from taking a ShareLock which in turn prevents other
writers from taking a RowExclusiveLock for a long time. Instead, the
reader can wait for the first writer without taking any locks or
blocking later writers. I've illustrated this difference in the
isolation tests.

Still hoping we can get this into 17. :-)

[0]: /messages/by-id/C8D65462-0888-4484-A72C-C99A94381ECD@yandex-team.ru

Attachments:

v10-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v10-0003-Add-pg_wait_for_lockers-function.patchDownload
From 0c72afeb37fb4c4c66efb77dd0b786b17764527a Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v10 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, using the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  82 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  94 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/wait-for-lockers.spec     |  79 +++++++
 .../regress/expected/wait_for_lockers.out     | 192 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  90 ++++++++
 11 files changed, 749 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0bb7aeb40e..c2621a7a0f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29430,6 +29430,88 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index c96c98e6dc..b839587453 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4036,6 +4036,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 13009cc3d0..bd3eb64582 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -13,12 +13,17 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +606,95 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are primarily just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS)
+{
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+	List	   *locktags = NIL;
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291ed876fc..762ffef8f7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index de40dfaa16..7d0e0ed741 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -590,6 +590,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..a3e892549d
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1..dd08f2dfe8 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..c02bd884df
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..24415edd7a
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,192 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..43e4493205 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..a299cd7863
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,90 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

v10-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchapplication/octet-stream; name=v10-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchDownload
From f0283f8c0bef98a08e264f159fe9812e621fd9a5 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v10 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

Support getting lockers in a single specified lock mode, rather than all
modes that conflict with a specified mode.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  8 ++--
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 54 insertions(+), 40 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 8090ac9fc1..5e7a8331fd 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -454,7 +454,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->vxid.lxid = xid;
 		proc->vxid.procNumber = INVALID_PROC_NUMBER;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 1d930752c5..86174ca82b 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2598,8 +2598,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 87b04e51b3..17f0762edd 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -656,7 +656,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -710,7 +710,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 41fd856c65..53d838e971 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -922,8 +922,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -932,8 +932,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 0d93932d8d..c96c98e6dc 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2831,43 +2831,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2888,19 +2895,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2953,12 +2968,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2973,7 +2988,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3007,11 +3022,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3020,8 +3035,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3037,7 +3050,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].procNumber = INVALID_PROC_NUMBER;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index f3e20038f4..718c9fa6dd 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1317,8 +1317,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 0017d4b868..de40dfaa16 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -573,8 +573,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v10-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchapplication/octet-stream; name=v10-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchDownload
From 97ec90fc51b044d71035918c2c42c633cf252338 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v10 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lock mode, rather than all lock
modes that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 21 +++++++++++----------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index e6140853c0..ef4314f480 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2299,7 +2299,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2315,7 +2315,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 943a48bfa7..4e8a3de74c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1677,7 +1677,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1724,7 +1724,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -4043,7 +4043,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4102,7 +4102,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4260,7 +4260,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4294,7 +4294,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7014be8039..3152151e41 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19626,7 +19626,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 53d838e971..626340eb16 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -892,19 +892,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -922,7 +923,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -982,16 +983,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMASK lockmask, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmask, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..7468a0500f 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting ,bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

#26Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#25)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

Rebased, fixed a couple typos, and reordered the isolation tests to
put the most elaborate pair last.

Attachments:

v11-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchapplication/octet-stream; name=v11-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchDownload
From b1cf1ec60050dc3772e58232971c8c4a0b6ce010 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v11 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

Support getting lockers in a single specified lock mode, rather than all
modes that conflict with a specified mode.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  8 ++--
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 54 insertions(+), 40 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 8090ac9fc1..5e7a8331fd 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -454,7 +454,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->vxid.lxid = xid;
 		proc->vxid.procNumber = INVALID_PROC_NUMBER;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 1d930752c5..86174ca82b 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2598,8 +2598,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 87b04e51b3..17f0762edd 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -656,7 +656,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -710,7 +710,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 41fd856c65..53d838e971 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -922,8 +922,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -932,8 +932,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 5022a50dd7..8794a389a7 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2850,43 +2850,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2907,19 +2914,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2972,12 +2987,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2992,7 +3007,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3026,11 +3041,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3039,8 +3054,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3056,7 +3069,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].procNumber = INVALID_PROC_NUMBER;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 162b1f919d..c08c0347ec 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1333,8 +1333,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 0017d4b868..de40dfaa16 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -573,8 +573,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v11-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchapplication/octet-stream; name=v11-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchDownload
From 92e9a07d3d6b1e11cab83b3a49a705665ae22e6a Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v11 2/3] Allow specifying single lockmode in WaitForLockers()

Allow waiting for a single specified lock mode, rather than all lock
modes that conflict with a specified mode.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 21 +++++++++++----------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index b6a7c60e23..72e4c92c75 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2272,7 +2272,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2288,7 +2288,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d9016ef487..81711e9afc 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1678,7 +1678,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1725,7 +1725,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -4054,7 +4054,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4113,7 +4113,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4271,7 +4271,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4305,7 +4305,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8a02c5b05b..1e4a69c2b7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19837,7 +19837,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 53d838e971..5f552f4d7c 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -892,19 +892,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -922,7 +923,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -982,16 +983,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmode, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index e8bd71ba68..2f88b9b6e3 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting, bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v11-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v11-0003-Add-pg_wait_for_lockers-function.patchDownload
From 903eafc0e2967b845d6bfcfe55bf28c16624c13e Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v11 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, using the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

See docs and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  82 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  95 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/wait-for-lockers.spec     |  79 +++++++
 .../regress/expected/wait_for_lockers.out     | 192 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  90 ++++++++
 11 files changed, 750 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8ecc02f2b9..6fc634eebd 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29781,6 +29781,88 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para>
+       <para>
+        To wait for locks on a table, the user must have
+        <literal>SELECT</literal>, <literal>INSERT</literal>,
+        <literal>UPDATE</literal>, <literal>DELETE</literal>, or
+        <literal>TRUNCATE</literal> privileges on the table.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 8794a389a7..de113f7bc9 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4055,6 +4055,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 13009cc3d0..028453f33a 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -13,12 +13,17 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +606,96 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are mostly just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS)
+{
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+	List	   *locktags = NIL;
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		AclResult	aclresult;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers on %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		/* Require some permission */
+		aclresult = pg_class_aclcheck(reloid, GetUserId(),
+									  (ACL_SELECT | ACL_UPDATE | ACL_INSERT |
+									   ACL_DELETE | ACL_TRUNCATE));
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2f7cfc02c6..d416ffd95a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6304,6 +6304,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index de40dfaa16..7d0e0ed741 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -590,6 +590,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..5cd82c270c
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 188fc04f85..17aba6af82 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..42936e3b46
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..24415edd7a
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,192 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ERROR:  permission denied for table wfl_tbl1
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers on pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..b72e7cd8c9 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..a299cd7863
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,90 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE SCHEMA wfl_schema1;
+SET search_path = wfl_schema1;
+CREATE TABLE wfl_tbl1 (a BIGINT);
+CREATE ROLE regress_rol_wfl1;
+ALTER ROLE regress_rol_wfl1 SET search_path = wfl_schema1;
+GRANT USAGE ON SCHEMA wfl_schema1 TO regress_rol_wfl1;
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- pg_wait_for_lockers() requires some permissions regardless of lock mode
+-- fail without permissions
+SET ROLE regress_rol_wfl1;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ROLLBACK;
+RESET ROLE;
+-- succeed with only SELECT permissions and ACCESS EXCLUSIVE mode
+GRANT SELECT ON TABLE wfl_tbl1 TO regress_rol_wfl1;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+RESET ROLE;
+REVOKE SELECT ON TABLE wfl_tbl1 FROM regress_rol_wfl1;
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
+DROP SCHEMA wfl_schema1 CASCADE;
+DROP ROLE regress_rol_wfl1;
-- 
2.34.1

#27Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#1)
3 attachment(s)
Re: Exposing the lock manager's WaitForLockers() to SQL

I got some very helpful off-list feedback from Robert Haas that this
needed more self-contained explanation/motivation. So here goes. :-)

This patch set adds a new SQL function pg_wait_for_lockers(), which
waits for transactions holding specified table locks to commit or roll
back. This can be useful with knowledge of the queries in those
transactions, particularly for asynchronous and incremental processing
of inserted/updated rows.

Specifically, consider a scenario where INSERTs and UPDATEs always set
a serial column to its default value. A client can call
pg_sequence_last_value() + pg_wait_for_lockers() and then take a new
DB snapshot and know that rows committed after this snapshot will have
values of the serial column greater than the value from
pg_sequence_last_value(). As shown in the example at the end, this
allows the client to asynchronously and incrementally read
inserted/updated rows with minimal per-client state, without buffering
changes, and without affecting writer transactions.

There are lots of other ways to support incrementally reading new
rows, but they don’t have all of those qualities. For example:

* Forcing writers to commit in a specific order (e.g. by serial column
value) would reduce throughput

* Explicitly tracking or coordinating with writers would likely be
more complex, impact performance, and/or require much more state

* Methods that are synchronous or buffer/queue changes are problematic
if readers fall behind

Existing ways to wait for table locks also have downsides:

* Taking a conflicting lock with LOCK blocks new transactions from
taking the lock of interest while LOCK waits. And in order to wait for
writers holding RowExclusiveLock, we must take ShareLock, which also
conflicts with ShareUpdateExclusiveLock and therefore unnecessarily
interferes with (auto)vacuum. Finally, with multiple tables LOCK locks
them one at a time, so it waits (and holds locks) longer than
necessary.

* Using pg_locks / pg_lock_status() to identify the transactions
holding the locks is more expensive since it also returns all other
locks in the DB cluster, plus there’s no efficient built-in way to
wait for the transactions to commit or roll back.

By contrast, pg_wait_for_lockers() doesn’t block other transactions,
waits on multiple tables in parallel, and doesn’t spend time looking
at irrelevant locks.

This change is split into three patches for ease of review. The first
two patches modify the existing WaitForLockers() C function and other
locking internals to support waiting for lockers in a single lock
mode, which allows waiting for INSERT/UPDATE without waiting for
vacuuming. These changes could be omitted at the cost of unnecessary
waiting, potentially for a long time with slow vacuums. The third
patch adds the pg_wait_for_lockers() SQL function, which just calls
WaitForLockers().

FWIW, another solution might be to directly expose the functions that
WaitForLockers() calls, namely GetLockConflicts() (generalized to
GetLockers() in the first patch) to identify the transactions holding
the locks, and VirtualXactLock() to wait for each transaction to
commit or roll back. That would be more complicated for the client but
could be more broadly useful. I could investigate that further if it
seems preferable.

=== Example ===

Assume we have the following table:

CREATE TABLE page_views (
id bigserial,
view_time timestamptz
);

which is only ever modified by (potentially concurrent) INSERT
commands that assign the default value to the id column. We can run
the following commands:

SELECT pg_sequence_last_value('page_views_id_seq');

pg_sequence_last_value
------------------------
4

SELECT pg_wait_for_lockers(array['page_views']::oid, regclass[],
'RowExclusiveLock', FALSE);

Now we know that all rows where id <= 4 have been committed or rolled
back, and we can observe/process them:

SELECT * FROM page_views WHERE id <= 4;

id | view_time
----+-------------------------------
2 | 2024-01-01 12:34:01.000000-00
3 | 2024-01-01 12:34:00.000000-00

Later we can iterate:

SELECT pg_sequence_last_value('page_views_id_seq');

pg_sequence_last_value
------------------------
9

SELECT pg_wait_for_lockers(array['page_views']::oid, regclass[],
'RowExclusiveLock', FALSE);

We already observed all the rows where id <= 4, so this time we can
filter them out:

SELECT * FROM page_views WHERE id > 4 AND id <= 9;

id | view_time
----+-------------------------------
5 | 2024-01-01 12:34:05.000000-00
8 | 2024-01-01 12:34:04.000000-00
9 | 2024-01-01 12:34:07.000000-00

We can continue iterating like this to incrementally observe more
newly inserted rows. Note that the only state we persist across
iterations is the value returned by pg_sequence_last_value().

In this example, we processed inserted rows exactly once. Variations
are possible for handling updates, as discussed in the original email,
and I could explain that again better if it would be helpful. :-)

Attachments:

v12-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchapplication/octet-stream; name=v12-0002-Allow-specifying-single-lockmode-in-WaitForLocke.patchDownload
From 44d7674e0eefd798a3da067ce572c56afa814d4a Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:12:49 -0800
Subject: [PATCH v12 2/3] Allow specifying single lockmode in WaitForLockers()

As with the previous patch, allow waiting for a single specified lock
mode, rather than all lock modes that conflict with a specified mode.
This is separated only for ease of review.
---
 src/backend/catalog/index.c      |  4 ++--
 src/backend/commands/indexcmds.c | 12 ++++++------
 src/backend/commands/tablecmds.c |  3 ++-
 src/backend/storage/lmgr/lmgr.c  | 21 +++++++++++----------
 src/include/storage/lmgr.h       |  6 ++++--
 5 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 55fdde4b24..7c1562f23e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2268,7 +2268,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * here, even though it will only be used when we're called by REINDEX
 		 * CONCURRENTLY and not when called by DROP INDEX CONCURRENTLY.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/* Finish invalidation of index and mark it as dead */
 		index_concurrently_set_dead(heapId, indexId);
@@ -2284,7 +2284,7 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 		 * Wait till every transaction that saw the old index state has
 		 * finished.  See above about progress reporting.
 		 */
-		WaitForLockers(heaplocktag, AccessExclusiveLock, true);
+		WaitForLockers(heaplocktag, AccessExclusiveLock, true, true);
 
 		/*
 		 * Re-open relations to allow us to complete our actions.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 309389e20d..1b18011c89 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1664,7 +1664,7 @@ DefineIndex(Oid tableId,
 	 * exclusive lock on our table.  The lock code will detect deadlock and
 	 * error out properly.
 	 */
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * At this moment we are sure that there are no transactions with the
@@ -1711,7 +1711,7 @@ DefineIndex(Oid tableId,
 	 */
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockers(heaplocktag, ShareLock, true);
+	WaitForLockers(heaplocktag, ShareLock, true, true);
 
 	/*
 	 * Now take the "reference snapshot" that will be used by validate_index()
@@ -3934,7 +3934,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -3993,7 +3993,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
-	WaitForLockersMultiple(lockTags, ShareLock, true);
+	WaitForLockersMultiple(lockTags, ShareLock, true, true);
 	CommitTransactionCommand();
 
 	foreach(lc, newIndexIds)
@@ -4151,7 +4151,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	foreach(lc, indexIds)
 	{
@@ -4185,7 +4185,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
 								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
-	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true);
+	WaitForLockersMultiple(lockTags, AccessExclusiveLock, true, true);
 
 	PushActiveSnapshot(GetTransactionSnapshot());
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7b6c69b7a5..bf4ae13752 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19074,7 +19074,8 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * partition itself, since we will acquire AccessExclusiveLock below.
 		 */
 		SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid);
-		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false);
+		WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, true,
+							   false);
 
 		/*
 		 * Now acquire locks in both relations again.  Note they may have been
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index 720fb1c6a9..23838ac9fe 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -892,19 +892,20 @@ XactLockTableWaitErrorCb(void *arg)
 
 /*
  * WaitForLockersMultiple
- *		Wait until no transaction holds locks that conflict with the given
- *		locktags at the given lockmode.
+ *		Wait until no transaction holds locks on the given locktags, either in
+ *		or conflicting with the given lockmode, depending on the value of the
+ *		conflicting argument.
  *
  * To do this, obtain the current list of lockers, and wait on their VXIDs
  * until they are finished.
  *
  * Note we don't try to acquire the locks on the given locktags, only the
- * VXIDs and XIDs of their lock holders; if somebody grabs a conflicting lock
- * on the objects after we obtained our initial list of lockers, we will not
- * wait for them.
+ * VXIDs and XIDs of their lock holders; if somebody grabs a lock on the objects
+ * after we obtained our initial list of lockers, we will not wait for them.
  */
 void
-WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
+WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool conflicting,
+					   bool progress)
 {
 	List	   *holders = NIL;
 	ListCell   *lc;
@@ -922,7 +923,7 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockers(locktag, lockmode, true,
+						  GetLockers(locktag, lockmode, conflicting,
 									 progress ? &count : NULL));
 		if (progress)
 			total += count;
@@ -982,16 +983,16 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
  * Same as WaitForLockersMultiple, for a single lock tag.
  */
 void
-WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress)
+WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool conflicting,
+			   bool progress)
 {
 	List	   *l;
 
 	l = list_make1(&heaplocktag);
-	WaitForLockersMultiple(l, lockmode, progress);
+	WaitForLockersMultiple(l, lockmode, conflicting, progress);
 	list_free(l);
 }
 
-
 /*
  *		LockDatabaseObject
  *
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 22b7856ef1..c0129dbaeb 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -82,8 +82,10 @@ extern void XactLockTableWait(TransactionId xid, Relation rel,
 extern bool ConditionalXactLockTableWait(TransactionId xid);
 
 /* Lock VXIDs, specified by conflicting locktags */
-extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode, bool progress);
-extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress);
+extern void WaitForLockers(LOCKTAG heaplocktag, LOCKMODE lockmode,
+						   bool conflicting, bool progress);
+extern void WaitForLockersMultiple(List *locktags, LOCKMODE lockmode,
+								   bool conflicting, bool progress);
 
 /* Lock an XID for tuple insertion (used to wait for an insertion to finish) */
 extern uint32 SpeculativeInsertionLockAcquire(TransactionId xid);
-- 
2.34.1

v12-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchapplication/octet-stream; name=v12-0001-Refactor-GetLockConflicts-into-more-general-GetL.patchDownload
From 08799a70408b55fd2ae9101c3481b283088c2186 Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Thu, 21 Dec 2023 22:08:51 -0800
Subject: [PATCH v12 1/3] Refactor GetLockConflicts() into more general
 GetLockers()

Support getting lockers in a single specified lock mode, rather than all
modes that conflict with a specified mode. This allows getting
transactions that hold RowExclusiveLock without also getting
transactions that hold ShareUpdateExclusiveLock (held by
vacuum/autovacuum), among other modes.
---
 src/backend/access/transam/twophase.c |  2 +-
 src/backend/access/transam/xact.c     |  4 +-
 src/backend/storage/ipc/standby.c     |  4 +-
 src/backend/storage/lmgr/lmgr.c       |  8 ++--
 src/backend/storage/lmgr/lock.c       | 67 ++++++++++++++++-----------
 src/backend/storage/lmgr/proc.c       |  4 +-
 src/include/storage/lock.h            |  5 +-
 7 files changed, 54 insertions(+), 40 deletions(-)

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index bf451d42ff..44d5da8730 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -454,7 +454,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid,
 	else
 	{
 		Assert(AmStartupProcess() || !IsPostmasterEnvironment);
-		/* GetLockConflicts() uses this to specify a wait on the XID */
+		/* GetLockers() uses this to specify a wait on the XID */
 		proc->vxid.lxid = xid;
 		proc->vxid.procNumber = INVALID_PROC_NUMBER;
 	}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 4f4ce75762..baba0765dd 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2636,8 +2636,8 @@ PrepareTransaction(void)
 
 	/*
 	 * Transfer our locks to a dummy PGPROC.  This has to be done before
-	 * ProcArrayClearTransaction().  Otherwise, a GetLockConflicts() would
-	 * conclude "xact already committed or aborted" for our locks.
+	 * ProcArrayClearTransaction().  Otherwise, a GetLockers() would conclude
+	 * "xact already committed or aborted" for our locks.
 	 */
 	PostPrepare_Locks(xid);
 
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 87b04e51b3..17f0762edd 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -656,7 +656,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 		 */
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/*
 		 * Prevent ResolveRecoveryConflictWithVirtualXIDs() from reporting
@@ -710,7 +710,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
 	{
 		VirtualTransactionId *backends;
 
-		backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+		backends = GetLockers(&locktag, AccessExclusiveLock, true, NULL);
 
 		/* Quick exit if there's no work to be done */
 		if (!VirtualTransactionIdIsValid(*backends))
diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c
index fe3cda2f88..720fb1c6a9 100644
--- a/src/backend/storage/lmgr/lmgr.c
+++ b/src/backend/storage/lmgr/lmgr.c
@@ -922,8 +922,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		int			count;
 
 		holders = lappend(holders,
-						  GetLockConflicts(locktag, lockmode,
-										   progress ? &count : NULL));
+						  GetLockers(locktag, lockmode, true,
+									 progress ? &count : NULL));
 		if (progress)
 			total += count;
 	}
@@ -932,8 +932,8 @@ WaitForLockersMultiple(List *locktags, LOCKMODE lockmode, bool progress)
 		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, total);
 
 	/*
-	 * Note: GetLockConflicts() never reports our own xid, hence we need not
-	 * check for that.  Also, prepared xacts are reported and awaited.
+	 * Note: GetLockers() never reports our own xid, hence we need not check for
+	 * that.  Also, prepared xacts are reported and awaited.
 	 */
 
 	/* Finally wait for each such transaction to complete */
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index f68c595c8a..eed642acb7 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -2850,43 +2850,50 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
 }
 
 /*
- * GetLockConflicts
+ * GetLockers
  *		Get an array of VirtualTransactionIds of xacts currently holding locks
- *		that would conflict with the specified lock/lockmode.
- *		xacts merely awaiting such a lock are NOT reported.
+ *		on the specified locktag either in or conflicting with the given
+ *		lockmode, depending on the value of the conflicting argument. xacts
+ *		merely awaiting such a lock are NOT reported.
  *
  * The result array is palloc'd and is terminated with an invalid VXID.
  * *countp, if not null, is updated to the number of items set.
  *
  * Of course, the result could be out of date by the time it's returned, so
  * use of this function has to be thought about carefully.  Similarly, a
- * PGPROC with no "lxid" will be considered non-conflicting regardless of any
- * lock it holds.  Existing callers don't care about a locker after that
- * locker's pg_xact updates complete.  CommitTransaction() clears "lxid" after
- * pg_xact updates and before releasing locks.
+ * PGPROC with no "lxid" will not be returned regardless of any lock it holds.
+ * Existing callers don't care about a locker after that locker's pg_xact
+ * updates complete.  CommitTransaction() clears "lxid" after pg_xact updates
+ * and before releasing locks.
  *
- * Note we never include the current xact's vxid in the result array,
- * since an xact never blocks itself.
+ * Note we never include the current xact's vxid in the result array, because
+ * existing callers don't care to know about it, since an xact never blocks
+ * itself and can see its own uncommitted changes.
  */
 VirtualTransactionId *
-GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
+GetLockers(const LOCKTAG *locktag, LOCKMODE lockmode, bool conflicting,
+		   int *countp)
 {
 	static VirtualTransactionId *vxids;
 	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
 	LockMethod	lockMethodTable;
+	int			numLockModes;
 	LOCK	   *lock;
-	LOCKMASK	conflictMask;
+	LOCKMASK	getMask;
 	dlist_iter	proclock_iter;
 	PROCLOCK   *proclock;
 	uint32		hashcode;
 	LWLock	   *partitionLock;
 	int			count = 0;
+	int			i;
+	bool		checkFast = false;
 	int			fast_count = 0;
 
 	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
 		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
 	lockMethodTable = LockMethods[lockmethodid];
-	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
+	numLockModes = lockMethodTable->numLockModes;
+	if (lockmode <= 0 || lockmode > numLockModes)
 		elog(ERROR, "unrecognized lock mode: %d", lockmode);
 
 	/*
@@ -2907,19 +2914,27 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 			palloc0(sizeof(VirtualTransactionId) *
 					(MaxBackends + max_prepared_xacts + 1));
 
-	/* Compute hash code and partition lock, and look up conflicting modes. */
+	/* Compute hash code and partition lock, and construct lock mask */
 	hashcode = LockTagHashCode(locktag);
 	partitionLock = LockHashPartitionLock(hashcode);
-	conflictMask = lockMethodTable->conflictTab[lockmode];
+	getMask = conflicting ? lockMethodTable->conflictTab[lockmode] :
+		LOCKBIT_ON(lockmode);
 
 	/*
 	 * Fast path locks might not have been entered in the primary lock table.
-	 * If the lock we're dealing with could conflict with such a lock, we must
-	 * examine each backend's fast-path array for conflicts.
+	 * If getMask could match such a lock, we must examine each backend's
+	 * fast-path array.
 	 */
-	if (ConflictsWithRelationFastPath(locktag, lockmode))
+	for (i = 1; i <= numLockModes; i++)
+	{
+		if (((getMask & LOCKBIT_ON(i)) != 0) &&
+			EligibleForRelationFastPath(locktag, i)) {
+			checkFast = true;
+			break;
+		}
+	}
+	if (checkFast)
 	{
-		int			i;
 		Oid			relid = locktag->locktag_field2;
 		VirtualTransactionId vxid;
 
@@ -2972,12 +2987,12 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				/*
 				 * There can only be one entry per relation, so if we found it
-				 * and it doesn't conflict, we can skip the rest of the slots.
+				 * and it doesn't match, we can skip the rest of the slots.
 				 */
-				if ((lockmask & conflictMask) == 0)
+				if ((lockmask & getMask) == 0)
 					break;
 
-				/* Conflict! */
+				/* Match! */
 				GET_VXID_FROM_PGPROC(vxid, *proc);
 
 				if (VirtualTransactionIdIsValid(vxid))
@@ -2992,7 +3007,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 		}
 	}
 
-	/* Remember how many fast-path conflicts we found. */
+	/* Remember how many fast-path matches we found. */
 	fast_count = count;
 
 	/*
@@ -3026,11 +3041,11 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	{
 		proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur);
 
-		if (conflictMask & proclock->holdMask)
+		if (getMask & proclock->holdMask)
 		{
 			PGPROC	   *proc = proclock->tag.myProc;
 
-			/* A backend never blocks itself */
+			/* A backend doesn't care about its own locks */
 			if (proc != MyProc)
 			{
 				VirtualTransactionId vxid;
@@ -3039,8 +3054,6 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 
 				if (VirtualTransactionIdIsValid(vxid))
 				{
-					int			i;
-
 					/* Avoid duplicate entries. */
 					for (i = 0; i < fast_count; ++i)
 						if (VirtualTransactionIdEquals(vxids[i], vxid))
@@ -3056,7 +3069,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
 	LWLockRelease(partitionLock);
 
 	if (count > MaxBackends + max_prepared_xacts)	/* should never happen */
-		elog(PANIC, "too many conflicting locks found");
+		elog(PANIC, "too many locks found");
 
 	vxids[count].procNumber = INVALID_PROC_NUMBER;
 	vxids[count].localTransactionId = InvalidLocalTransactionId;
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index ce29da9012..4964cb9417 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1333,8 +1333,8 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait)
 					VirtualTransactionId *vxids;
 					int			cnt;
 
-					vxids = GetLockConflicts(&locallock->tag.lock,
-											 AccessExclusiveLock, &cnt);
+					vxids = GetLockers(&locallock->tag.lock,
+									   AccessExclusiveLock, true, &cnt);
 
 					/*
 					 * Log the recovery conflict and the list of PIDs of
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 0017d4b868..de40dfaa16 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -573,8 +573,9 @@ extern HTAB *GetLockMethodLocalHash(void);
 #endif
 extern bool LockHasWaiters(const LOCKTAG *locktag,
 						   LOCKMODE lockmode, bool sessionLock);
-extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag,
-											  LOCKMODE lockmode, int *countp);
+extern VirtualTransactionId *GetLockers(const LOCKTAG *locktag,
+										LOCKMODE lockmode, bool conflicting,
+										int *countp);
 extern void AtPrepare_Locks(void);
 extern void PostPrepare_Locks(TransactionId xid);
 extern bool LockCheckConflicts(LockMethod lockMethodTable,
-- 
2.34.1

v12-0003-Add-pg_wait_for_lockers-function.patchapplication/octet-stream; name=v12-0003-Add-pg_wait_for_lockers-function.patchDownload
From 75431139689cb2aef34bda870c5e8dac006a711d Mon Sep 17 00:00:00 2001
From: Will Mortensen <will@extrahop.com>
Date: Wed, 10 Jan 2024 23:38:13 -0800
Subject: [PATCH v12 3/3] Add pg_wait_for_lockers() function

Rather than actually taking any locks on the table(s), it simply waits
for already-held locks to be released, by simply calling the existing
WaitForLockersMultiple() function in the lock manager.

Currently it's only supported on plain tables and doesn't automatically
handle inheritance/partition relationships.

No permissions on the table are required because the same goal can be
achieved (much less efficiently) by repeatedly polling pg_locks, which
doesn't seem to require any permissions. Note that the error messages
reveal the relation name and kind but I think this is OK because these
are visible in the system catalogs?

See email, docs, and tests for more detail.
---
 doc/src/sgml/func.sgml                        |  76 ++++++++
 src/backend/storage/lmgr/lock.c               |  23 +++
 src/backend/utils/adt/lockfuncs.c             |  87 +++++++++
 src/include/catalog/pg_proc.dat               |   5 +
 src/include/storage/lock.h                    |   2 +
 .../isolation/expected/wait-for-lockers.out   | 180 ++++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/wait-for-lockers.spec     |  79 ++++++++
 .../regress/expected/wait_for_lockers.out     | 157 +++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/wait_for_lockers.sql     |  66 +++++++
 11 files changed, 677 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/wait-for-lockers.out
 create mode 100644 src/test/isolation/specs/wait-for-lockers.spec
 create mode 100644 src/test/regress/expected/wait_for_lockers.out
 create mode 100644 src/test/regress/sql/wait_for_lockers.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 17c44bc338..c1b71f6aa0 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30320,6 +30320,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-table-lock">
+   <title>Table Lock Functions</title>
+
+   <indexterm>
+    <primary>Table Lock Functions</primary>
+   </indexterm>
+
+   <para>
+    The functions shown in <xref linkend="functions-table-lock-table"/> interact
+    with table locks. For details about these locks, see
+    <xref linkend="locking-tables"/>.
+   </para>
+
+   <table id="functions-table-lock-table">
+    <title>Table Lock Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_wait_for_lockers</primary>
+        </indexterm>
+        <function>pg_wait_for_lockers</function> ( <parameter>tables</parameter> <type>regclass[]</type>, <parameter>lockmode</parameter> <type>text</type>, <parameter>conflicting</parameter> <type>boolean</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Waits for already-held table-level locks on the specified tables to be
+        released, and does not take any new table-level locks.
+       </para>
+       <para>
+        <parameter>lockmode</parameter> must be a string that specifies a
+        table-level lock mode, such as <literal>'ShareLock'</literal>. If
+        <parameter>conflicting</parameter> is true, the function waits for locks
+        in modes that conflict with <parameter>lockmode</parameter>; otherwise
+        it waits for locks in <parameter>lockmode</parameter>. Note that a lock
+        mode may or may not conflict with itself.
+       </para>
+       <para>
+        The function first builds a set of transactions that hold matching
+        locks on any of the tables, and then waits for the transactions in the
+        set to release those locks. The set does not include any transaction
+        that is only waiting to take a matching lock but does not yet hold one,
+        nor any transaction that only takes a matching lock after the function
+        finishes building the set. The set may or may not include a transaction
+        that only takes a matching lock while the function is building the set.
+        The set never includes the transaction that is calling the function,
+        even if it holds a matching lock, to avoid a trivial deadlock.
+       </para>
+       <para>
+        The function waits for table-level locks on only the specified tables.
+        To wait for locks on ancestor or descendant tables (if any), you must
+        specify them explicitly.
+       </para>
+       <para>
+        Since the function does not take any table-level locks, a table may be
+        dropped by another transaction while waiting. Once the drop commits,
+        there can be no more locks on the table to wait for.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   <sect2 id="functions-advisory-locks">
    <title>Advisory Lock Functions</title>
 
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index eed642acb7..03d9718778 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -4055,6 +4055,29 @@ GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode)
 	return LockMethods[lockmethodid]->lockModeNames[mode];
 }
 
+/*
+ * Convert the (case-insensitive) textual name of any lock mode to the LOCKMODE
+ * value
+ */
+LOCKMODE
+ParseLockmodeName(LOCKMETHODID lockmethodid, const char *mode_name)
+{
+	int	i;
+	LockMethod	lockMethodTable;
+
+	Assert(lockmethodid > 0 && lockmethodid < lengthof(LockMethods));
+	lockMethodTable = LockMethods[lockmethodid];
+	for (i = 1; i <= lockMethodTable->numLockModes; i++)
+		if (pg_strcasecmp(mode_name, lockMethodTable->lockModeNames[i]) == 0)
+			return i;
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("invalid lock mode name %s", mode_name)));
+	/* unreachable but appease compiler */
+	return NoLock;
+}
+
 #ifdef LOCK_DEBUG
 /*
  * Dump all locks in the given proc's myProcLocks lists.
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 13009cc3d0..cac56120b4 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -13,12 +13,17 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
 #include "storage/predicate_internals.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -601,6 +606,88 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ * pg_wait_for_lockers - wait for already-held table-level locks without taking
+ * any new table-level locks
+ *
+ * The semantics are mostly just those of WaitForLockersMultiple(). For now,
+ * only plain tables are supported, and there is no automatic handling of
+ * descendant tables (if any).
+ *
+ * Since no table-level locks are taken, there are some inherent races. The
+ * specified tables must exist long enough to check their permissions, but could
+ * be dropped afterward.
+ */
+Datum
+pg_wait_for_lockers(PG_FUNCTION_ARGS)
+{
+	ArrayType  *reloids_a = PG_GETARG_ARRAYTYPE_P(0);
+	char	   *mode_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	bool		conflicting = PG_GETARG_BOOL(2);
+	List	   *locktags = NIL;
+	int			i;
+	Oid		   *reloids;
+	int			num_reloids;
+	LOCKMODE	mode;
+
+	/* Validate the passed-in array */
+	Assert(ARR_ELEMTYPE(reloids_a) == REGCLASSOID);
+	if (array_contains_nulls(reloids_a))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation oid must not be null")));
+	reloids = (Oid *) ARR_DATA_PTR(reloids_a);
+	num_reloids = ArrayGetNItems(ARR_NDIM(reloids_a), ARR_DIMS(reloids_a));
+
+	/*
+	 * Determine lock mode. We only support relations, which use the default
+	 * lock method.
+	 */
+	mode = ParseLockmodeName(DEFAULT_LOCKMETHOD, mode_str);
+
+	for (i = 0; i < num_reloids; i++)
+	{
+		char		relkind;
+		LOCKTAG	   *heaplocktag;
+		Oid			dbid;
+		Oid			reloid = reloids[i];
+		char	   *relname;
+
+		if (!OidIsValid(reloid))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d is invalid", reloid)));
+
+		relkind = get_rel_relkind(reloid);
+		/* For error messages only */
+		relname = get_rel_name(reloid);
+		if (!relkind || relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relation oid %d does not exist", reloid)));
+
+		if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					 errmsg("cannot wait for lockers of %s", relname),
+					 errdetail_relkind_not_supported(relkind)));
+
+		heaplocktag = palloc_object(LOCKTAG);
+		if (IsSharedRelation(reloid))
+			dbid = InvalidOid;
+		else
+			dbid = MyDatabaseId;
+		SET_LOCKTAG_RELATION(*heaplocktag, dbid, reloid);
+
+		locktags = lappend(locktags, heaplocktag);
+	}
+
+	WaitForLockersMultiple(locktags, mode, conflicting, false);
+
+	PG_RETURN_VOID();
+}
+
+
 /*
  * pg_isolation_test_session_is_blocked - support function for isolationtester
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6a5476d3c4..8cbd377fd8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6320,6 +6320,11 @@
   proname => 'pg_safe_snapshot_blocking_pids', provolatile => 'v',
   prorettype => '_int4', proargtypes => 'int4',
   prosrc => 'pg_safe_snapshot_blocking_pids' },
+{ oid => '7178',
+  descr => 'wait for already-held table locks to be released',
+  proname => 'pg_wait_for_lockers', provolatile => 'v',
+  prorettype => 'void', proargtypes => '_regclass text bool',
+  prosrc => 'pg_wait_for_lockers' },
 { oid => '3378', descr => 'isolationtester support function',
   proname => 'pg_isolation_test_session_is_blocked', provolatile => 'v',
   prorettype => 'bool', proargtypes => 'int4 _int4',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index de40dfaa16..7d0e0ed741 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -590,6 +590,8 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid);
 
 extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks);
 extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode);
+extern LOCKMODE ParseLockmodeName(LOCKMETHODID lockmethodid,
+								  const char *mode_name);
 
 extern void lock_twophase_recover(TransactionId xid, uint16 info,
 								  void *recdata, uint32 len);
diff --git a/src/test/isolation/expected/wait-for-lockers.out b/src/test/isolation/expected/wait-for-lockers.out
new file mode 100644
index 0000000000..5cd82c270c
--- /dev/null
+++ b/src/test/isolation/expected/wait-for-lockers.out
@@ -0,0 +1,180 @@
+Parsed test spec with 3 sessions
+
+starting permutation: w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE);
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wfl: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); <waiting ...>
+step w2_c: COMMIT;
+step r_wfl: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+ 2
+(2 rows)
+
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+(0 rows)
+
+step w2_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+step w1_lae1: LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE;
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT); <waiting ...>
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_c: COMMIT;
+step w2_in1: <... completed>
+step w2_c: COMMIT;
+step r_l: <... completed>
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_wflc: SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w1_in1: INSERT INTO t1 VALUES (DEFAULT);
+step w2_c: COMMIT;
+step r_wflc: <... completed>
+pg_wait_for_lockers
+-------------------
+                   
+(1 row)
+
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step w1_c: COMMIT;
+step r_c: COMMIT;
+
+starting permutation: w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
+step w2_in1: INSERT INTO t1 VALUES (DEFAULT);
+step r_sv: SAVEPOINT foo;
+step r_l: LOCK TABLE t1, t2 IN SHARE MODE; <waiting ...>
+step w1_lae2: LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE;
+step w2_c: COMMIT;
+step w1_c: COMMIT;
+step r_l: <... completed>
+step r_rb: ROLLBACK TO foo;
+step r_sel1: SELECT id from t1;
+id
+--
+ 1
+(1 row)
+
+step r_c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 0342eb39e4..fc185d2495 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -5,6 +5,7 @@ test: read-write-unique
 test: read-write-unique-2
 test: read-write-unique-3
 test: read-write-unique-4
+test: wait-for-lockers
 test: simple-write-skew
 test: receipt-report
 test: temporal-range-integrity
diff --git a/src/test/isolation/specs/wait-for-lockers.spec b/src/test/isolation/specs/wait-for-lockers.spec
new file mode 100644
index 0000000000..42936e3b46
--- /dev/null
+++ b/src/test/isolation/specs/wait-for-lockers.spec
@@ -0,0 +1,79 @@
+setup
+{
+	CREATE TABLE t1 (id bigserial);
+	CREATE TABLE t2 (id bigserial);
+}
+
+teardown
+{
+	DROP TABLE t1;
+	DROP TABLE t2;
+}
+
+# use READ COMMITTED so we can observe the effects of a committed INSERT after
+# waiting
+
+session writer1
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w1_in1		{ INSERT INTO t1 VALUES (DEFAULT); }
+step w1_lae1	{ LOCK TABLE t1 IN ACCESS EXCLUSIVE MODE; }
+step w1_lae2	{ LOCK TABLE t2 IN ACCESS EXCLUSIVE MODE; }
+step w1_c	{ COMMIT; }
+
+session writer2
+setup		{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step w2_in1	{ INSERT INTO t1 VALUES (DEFAULT); }
+step w2_c	{ COMMIT; }
+
+session reader
+setup			{ BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; }
+step r_sv		{ SAVEPOINT foo; }
+step r_l		{ LOCK TABLE t1, t2 IN SHARE MODE; }
+step r_rb		{ ROLLBACK TO foo; }
+step r_wfl		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'RowExclusiveLock', FALSE); }
+step r_wflc		{ SELECT pg_wait_for_lockers(array['t1','t2']::regclass[],
+											 'ShareLock', TRUE); }
+step r_sel1		{ SELECT id from t1; }
+step r_c		{ COMMIT; }
+
+
+# Basic sanity checks of pg_wait_for_lockers():
+
+# no waiting if no lockers (writers already committed)
+permutation w1_lae2 w2_in1 w2_c w1_c r_wflc r_sel1 r_c
+
+# reader waits only for writer2 holding a lock in ROW EXCLUSIVE mode, not for
+# writer1 holding a lock in ACCESS EXCLUSIVE mode
+permutation w1_lae2 w2_in1 r_wfl w2_c r_sel1 w1_c r_c
+
+# reader waits for both writers conflicting with SHARE mode
+permutation w1_lae2 w2_in1 r_wflc w2_c w1_c r_sel1 r_c
+
+
+# Comparisons between pg_wait_for_lockers() and nearest equivalent LOCK +
+# ROLLBACK:
+
+# reader waiting for writer1 allows writer2 to take a matching lock...
+permutation w1_in1 r_wflc w2_in1 w2_c w1_c r_sel1 r_c
+# ...whereas reader actually taking a conflicting lock blocks writer2 until
+# writer1 releases its lock (even if reader releases ASAP)
+permutation w1_in1 r_sv r_l w2_in1 w1_c r_rb w2_c r_sel1 r_c
+
+# reader waits only for matching lock already held by writer1, not for writer2
+# which was waiting to take a matching lock...
+permutation w1_lae1 w2_in1 r_wflc w1_c r_sel1 w2_c r_c
+# ...whereas actually taking a conflicting lock also waits for writer2 to take
+# and release its lock
+permutation w1_lae1 w2_in1 r_l w1_c w2_c r_sel1 r_c
+
+# reader waiting for two tables, with only writer2 holding a matching ROW
+# EXCLUSIVE lock on t1, allows writer1 to then take an ACCESS EXCLUSIVE lock on
+# t2 and another ROW EXCLUSIVE lock on t1, and reader doesn't wait for writer1's
+# later locks...
+permutation w2_in1 r_wflc w1_lae2 w1_in1 w2_c r_sel1 w1_c r_c
+# ...whereas reader actually taking conflicting locks on the two tables first
+# waits for writer2's ROW EXCLUSIVE lock (same as above), and then for writer1's
+# *later* ACCESS EXCLUSIVE lock (due to LOCK's one-by-one locking); note that
+# writer1's later insert w1_in1 would deadlock so it's omitted altogether
+permutation w2_in1 r_sv r_l w1_lae2 w2_c w1_c r_rb r_sel1 r_c
diff --git a/src/test/regress/expected/wait_for_lockers.out b/src/test/regress/expected/wait_for_lockers.out
new file mode 100644
index 0000000000..0e9e20bec0
--- /dev/null
+++ b/src/test/regress/expected/wait_for_lockers.out
@@ -0,0 +1,157 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+\set regresslib :libdir '/regress' :dlsuffix
+-- Setup
+CREATE TABLE wfl_tbl1 (a BIGINT);
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+ROLLBACK;
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ pg_wait_for_lockers 
+---------------------
+ 
+(1 row)
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 0 is invalid
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ERROR:  relation oid 987654321 does not exist
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ERROR:  cannot wait for lockers of pg_locks
+DETAIL:  This operation is not supported for views.
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ERROR:  invalid lock mode name AccessRowShareUpdateExclusiveLock
+ROLLBACK;
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 969ced994f..135446844e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse wait_for_lockers
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/wait_for_lockers.sql b/src/test/regress/sql/wait_for_lockers.sql
new file mode 100644
index 0000000000..d7b6cc5bc8
--- /dev/null
+++ b/src/test/regress/sql/wait_for_lockers.sql
@@ -0,0 +1,66 @@
+--
+-- Test the pg_wait_for_lockers() function
+--
+
+-- directory paths and dlsuffix are passed to us in environment variables
+\getenv libdir PG_LIBDIR
+\getenv dlsuffix PG_DLSUFFIX
+
+\set regresslib :libdir '/regress' :dlsuffix
+
+-- Setup
+CREATE TABLE wfl_tbl1 (a BIGINT);
+
+-- Try all valid options
+BEGIN TRANSACTION;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'RowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareUpdateExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ShareRowExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'ExclusiveLock', TRUE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() does nothing if the transaction itself is the only locker
+BEGIN TRANSACTION;
+LOCK TABLE wfl_tbl1 IN ACCESS EXCLUSIVE MODE;
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+ROLLBACK;
+
+-- pg_wait_for_lockers() is allowed outside a transaction
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', FALSE);
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessExclusiveLock', TRUE);
+
+-- fail gracefully with bogus arguments
+BEGIN;
+-- invalid oid
+select pg_wait_for_lockers('{0}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- nonexistent oid
+select pg_wait_for_lockers('{987654321}'::oid[]::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- views are not supported
+select pg_wait_for_lockers('{pg_locks}'::regclass[], 'AccessShareLock', FALSE);
+ROLLBACK;
+BEGIN;
+-- bogus lock mode
+select pg_wait_for_lockers('{wfl_tbl1}'::regclass[], 'AccessRowShareUpdateExclusiveLock', TRUE);
+ROLLBACK;
+
+--
+-- Clean up
+--
+DROP TABLE wfl_tbl1;
-- 
2.34.1

#28Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#27)
Re: Exposing the lock manager's WaitForLockers() to SQL

I should add that the latest patches remove permissions checks because
pg_locks doesn't have any, and improve the commit messages. Hope I
didn't garble anything doing this late after the dev conference. :-)

Robert asked me about other existing functions that could be
leveraged, such as GetConflictingVirtualXIDs(), but I didn't see any
with close-enough semantics that handle fast-path locks as needed for
tables/relations.

#29Will Mortensen
will@extrahop.com
In reply to: Will Mortensen (#27)
Re: Exposing the lock manager's WaitForLockers() to SQL

On Thu, May 30, 2024 at 12:01 AM Will Mortensen <will@extrahop.com> wrote:

FWIW, another solution might be to directly expose the functions that
WaitForLockers() calls, namely GetLockConflicts() (generalized to
GetLockers() in the first patch) to identify the transactions holding
the locks, and VirtualXactLock() to wait for each transaction to
commit or roll back. That would be more complicated for the client but
could be more broadly useful. I could investigate that further if it
seems preferable.

We will look further into this. Since the main advantage over polling
the existing pg_locks view would be efficiency, we will try to provide
more quantitative evidence/analysis of that. That will probably want
to be a new thread and CF entry, so I'm withdrawing this one.

Thanks again for all the replies, and to Robert for your off-list
feedback and letting me bend your ear in Vancouver. :-)