From 440d55ba360db9c06bf51c7bfd4221b197b5cae3 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Wed, 26 Aug 2026 12:19:43 +0800
Subject: [PATCH v2 1/3] Prevent WAIT FOR LSN from deadlocking recovery

A backend waiting for a standby 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 advance replay.  The
backend's latch-based LSN wait is invisible to the heavyweight-lock
deadlock detector, so the resulting cycle can persist indefinitely with
unlimited standby conflict delays.

Write and flush waits can also depend on startup.  During archive
recovery, their progress can come from the replay position.  If WAL
reception stops, startup may need to restart it before receiving can
continue.  A streaming receiver at wait entry therefore does not
guarantee independent progress throughout the wait.

Reject an unsatisfied standby_replay, standby_write, or standby_flush
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.  It
also rejects some write and flush waits that streaming could satisfy
independently, while allowing already-satisfied waits and preserving
primary_flush and 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 <noreply@openai.com>
---
 doc/src/sgml/ref/wait_for.sgml          | 13 ++++++++++++
 src/backend/commands/wait.c             | 20 ++++++++++++++++++
 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, 87 insertions(+)

diff --git a/doc/src/sgml/ref/wait_for.sgml b/doc/src/sgml/ref/wait_for.sgml
index 01dc2a84a1a..7f0039bf3bb 100644
--- a/doc/src/sgml/ref/wait_for.sgml
+++ b/doc/src/sgml/ref/wait_for.sgml
@@ -225,6 +225,19 @@ WAIT FOR LSN '<replaceable class="parameter">lsn</replaceable>'
    at isolation levels higher than <literal>READ COMMITTED</literal>.
   </para>
 
+  <para>
+   While recovery is in progress, an unsatisfied wait in
+   <literal>standby_replay</literal> (the default),
+   <literal>standby_write</literal>, or <literal>standby_flush</literal>
+   mode is rejected when the session already holds a heavyweight lock.  Such
+   a lock could make recovery depend, directly or indirectly, on the waiting
+   session.  Write and flush waits can also depend on recovery during archive
+   recovery or when WAL reception needs to be restarted.  This conservative
+   restriction also applies during streaming replication.  Execute
+   <command>WAIT FOR</command> before acquiring heavyweight locks, or release
+   them before waiting.
+  </para>
+
   <para>
    <command>WAIT FOR</command> waits until the specified
    <parameter>lsn</parameter> is reached according to the specified
diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c
index 9ba4c75021e..be7d9954237 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,25 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel,
 							 "Use standby_flush mode on a standby server.")));
 	}
 
+	/*
+	 * An unsatisfied standby LSN 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.
+	 * Write and flush waits can also depend on startup during archive recovery
+	 * or when WAL reception needs to be restarted.
+	 */
+	if ((lsnType == WAIT_LSN_TYPE_STANDBY_REPLAY ||
+		 lsnType == WAIT_LSN_TYPE_STANDBY_WRITE ||
+		 lsnType == WAIT_LSN_TYPE_STANDBY_FLUSH) &&
+		RecoveryInProgress() &&
+		lsn > GetCurrentLSNForWaitType(lsnType) &&
+		BackendHoldsGrantedHeavyweightLock())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("cannot wait for a standby LSN 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..c578320ac82 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 a standby LSN 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');],
+	stderr => \$stderr);
+like(
+	$stderr,
+	qr/cannot wait for a standby LSN 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

