From 64aa9d533d603cbda69c0eeb6b3badd4e6cc43c7 Mon Sep 17 00:00:00 2001 From: alterego655 <824662526@qq.com> Date: Wed, 26 Aug 2026 12:19:43 +0800 Subject: [PATCH v1 1/3] Prevent WAIT FOR LSN from deadlocking standby replay A backend waiting for a standby replay LSN can retain heavyweight locks acquired by earlier statements. The startup process may need one of those locks, directly or through another backend, before it can replay up to the requested LSN. This forms a cycle, but the backend's latch-based replay wait is invisible to the heavyweight-lock deadlock detector. With unlimited standby conflict delays, the cycle can persist indefinitely. Reject an unsatisfied standby_replay wait while recovery is active when the backend already holds a granted heavyweight lock. This conservative restriction covers direct relation-lock cycles and indirect cycles involving advisory locks, while allowing already-satisfied waits and preserving recovery-ended behavior. Add tests for relation and advisory locks, and document the restriction, noting that standby_replay is the default mode. Co-authored-by: ChatGPT 5.6 Sol --- doc/src/sgml/ref/wait_for.sgml | 9 +++++++++ src/backend/commands/wait.c | 16 +++++++++++++++ src/backend/storage/lmgr/lock.c | 27 +++++++++++++++++++++++++ src/include/storage/lock.h | 1 + src/test/recovery/t/049_wait_for_lsn.pl | 26 ++++++++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/doc/src/sgml/ref/wait_for.sgml b/doc/src/sgml/ref/wait_for.sgml index 01dc2a84a1a..a65e6167b88 100644 --- a/doc/src/sgml/ref/wait_for.sgml +++ b/doc/src/sgml/ref/wait_for.sgml @@ -225,6 +225,15 @@ WAIT FOR LSN 'lsn' at isolation levels higher than READ COMMITTED. + + The default mode is standby_replay. While recovery is + in progress, an unsatisfied wait in this mode is rejected when the session + already holds a heavyweight lock, because that lock could make recovery + depend, directly or indirectly, on the waiting session. Execute + WAIT FOR before acquiring heavyweight locks, or release + them before waiting. + + WAIT FOR waits until the specified lsn is reached according to the specified diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c index 9ba4c75021e..34a9d1c92a1 100644 --- a/src/backend/commands/wait.c +++ b/src/backend/commands/wait.c @@ -23,6 +23,7 @@ #include "commands/wait.h" #include "executor/executor.h" #include "parser/parse_node.h" +#include "storage/lock.h" #include "storage/proc.h" #include "utils/builtins.h" #include "utils/guc.h" @@ -194,6 +195,21 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel, "Use standby_flush mode on a standby server."))); } + /* + * An unsatisfied standby replay wait must not retain heavyweight locks. + * Such a lock could make recovery depend, directly or indirectly, on this + * backend while this backend is waiting for recovery to advance. + */ + if (lsnType == WAIT_LSN_TYPE_STANDBY_REPLAY && + RecoveryInProgress() && + lsn > GetCurrentLSNForWaitType(lsnType) && + BackendHoldsGrantedHeavyweightLock()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot wait for standby replay while holding heavyweight locks"), + errdetail("A heavyweight lock held by this session could participate in a cycle that prevents recovery from reaching the target LSN."), + errhint("Release the locks, or execute WAIT FOR before acquiring them."))); + /* Now wait for the LSN */ waitLSNResult = WaitForLSN(lsnType, lsn, timeout); diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index cf5c98c8176..d1c8ddb1381 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -673,6 +673,33 @@ LockHeldByMe(const LOCKTAG *locktag, return false; } +/* + * BackendHoldsGrantedHeavyweightLock -- check whether this backend owns any + * granted heavyweight lock represented in the local lock table. + * + * A LOCALLOCK entry can remain after an unsuccessful lock acquisition, so + * only entries with a positive local hold count represent locks we own. + */ +bool +BackendHoldsGrantedHeavyweightLock(void) +{ + HASH_SEQ_STATUS status; + LOCALLOCK *locallock; + + hash_seq_init(&status, LockMethodLocalHash); + + while ((locallock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + if (locallock->nLocks > 0) + { + hash_seq_term(&status); + return true; + } + } + + return false; +} + #ifdef USE_ASSERT_CHECKING /* * GetLockMethodLocalHash -- return the hash of local locks, for modules that diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..70909af48d5 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -401,6 +401,7 @@ extern void LockReleaseCurrentOwner(LOCALLOCK **locallocks, int nlocks); extern void LockReassignCurrentOwner(LOCALLOCK **locallocks, int nlocks); extern bool LockHeldByMe(const LOCKTAG *locktag, LOCKMODE lockmode, bool orstronger); +extern bool BackendHoldsGrantedHeavyweightLock(void); #ifdef USE_ASSERT_CHECKING extern HTAB *GetLockMethodLocalHash(void); #endif diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl index cb7d4d461de..e577ef74435 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -431,6 +431,32 @@ $node_standby->psql( ok( $stderr =~ /conflicting or redundant options/, "get error for duplicate MODE parameter"); +# An unsatisfied standby_replay wait must be rejected when the backend holds +# a heavyweight lock. The relation lock covers the direct two-process cycle, +# while the advisory lock is the return edge in the indirect three-process +# cycle. The short timeout keeps these tests bounded if the check regresses. +$node_standby->psql( + 'postgres', qq[ + BEGIN; + SELECT count(*) FROM wait_test; + WAIT FOR LSN '${lsn3}' WITH (timeout '100ms');], + stderr => \$stderr); +like( + $stderr, + qr/cannot wait for standby replay while holding heavyweight locks/, + "reject replay wait while holding a relation lock"); + +$node_standby->psql( + 'postgres', qq[ + BEGIN; + SELECT pg_advisory_xact_lock(42); + WAIT FOR LSN '${lsn3}' WITH (timeout '100ms', no_throw);], + stderr => \$stderr); +like( + $stderr, + qr/cannot wait for standby replay while holding heavyweight locks/, + "reject replay wait while holding an advisory lock"); + # 7a. Check the scenario of multiple standby_replay waiters. We make 5 # background psql sessions each waiting for a corresponding insertion. When # waiting is finished, stored procedures logs if there are visible as many -- 2.51.0