diff --git a/doc/src/sgml/ref/wait_for.sgml b/doc/src/sgml/ref/wait_for.sgml index 04ca9400426..0042825e702 100644 --- a/doc/src/sgml/ref/wait_for.sgml +++ b/doc/src/sgml/ref/wait_for.sgml @@ -225,6 +225,46 @@ WAIT FOR LSN 'lsn' at isolation levels higher than READ COMMITTED. + + While recovery is in progress, a wait in standby_replay + (the default), standby_write, or + standby_flush mode is rejected when the session already + holds a lock and the target lsn has not been reached + yet. Such a lock can make the startup process wait for this session, either + directly or through another session, while this session waits for the + startup process to advance recovery. That cycle involves no lock wait on + this side, so deadlock detection does not see it and nothing breaks it. A + wait whose target has already been reached returns immediately and is + therefore always allowed. + + + + Issue WAIT FOR outside a transaction block, or as the + first statement of one, before running anything that takes locks. That is + also the natural order for the read-your-writes pattern shown in the + examples below: wait for the target lsn first, then + run the queries that have to see it. Note that a lock taken by an earlier + statement is still held at READ COMMITTED, even though + its snapshot is gone, so a wait placed after such a statement is rejected + even when the isolation level permits it. + + + + The restriction covers standby_write and + standby_flush as well, even though streaming replication + can advance those positions without the startup process. Both positions are + at least the replay position, so without an active walreceiver replay can + be their only source of progress. If a held lock blocks replay, the session + waits for replay to advance while replay waits for the session to release + the lock. Under streaming replication the positions advance independently + only while WAL keeps arriving. If reception stops before the target is + reached, a blocked startup process cannot restart the walreceiver. It also + cannot replay newer checkpoint records needed to advance restartpoints and + recycle WAL, so pg_wal can fill up and reception can + stop before the target is reached. The restriction therefore also applies + when streaming is active at the start of the wait. + + 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..160447eddfb 100644 --- a/src/backend/commands/wait.c +++ b/src/backend/commands/wait.c @@ -23,6 +23,8 @@ #include "commands/wait.h" #include "executor/executor.h" #include "parser/parse_node.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "storage/proc.h" #include "utils/builtins.h" #include "utils/guc.h" @@ -194,6 +196,59 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel, "Use standby_flush mode on a standby server."))); } + /* + * Conservatively reject an unsatisfied standby LSN wait while this + * backend holds a granted heavyweight lock. Recovery may need one of + * those locks, directly or through another backend, before replay can + * advance far enough to satisfy our wait. This can create a cycle: we + * wait for recovery, while recovery waits for us to release the lock. + * + * However, we do not register our dependency on WAL progress as a lock + * wait, so the deadlock detector cannot see the complete cycle. With + * unlimited recovery-conflict delays and no other timeout or + * cancellation, the cycle can persist indefinitely. + * + * Write and flush waits can also depend on startup. Without an active + * receiver, their replay floor can be their only source of progress, so + * holding a lock needed by replay can create the same cycle. + * + * Streaming can initially provide independent progress, but reception can + * stop before the target is reached. Restarting reception requires + * startup, and stalled replay prevents further advancement of + * restartpoints used to recycle old WAL, so continued reception can + * exhaust available space. An active receiver at the start of the wait + * therefore does not guarantee that the wait can finish while replay + * remains blocked. + * + * Apply the restriction to all standby modes, including some write and + * flush waits that an active receiver could satisfy while locks remain + * held. Requests whose target is observed as already reached are exempt + * from this restriction. + */ + if ((lsnType == WAIT_LSN_TYPE_STANDBY_REPLAY || + lsnType == WAIT_LSN_TYPE_STANDBY_WRITE || + lsnType == WAIT_LSN_TYPE_STANDBY_FLUSH) && + RecoveryInProgress() && + lsn > GetCurrentLSNForWaitType(lsnType)) + { + LOCKTAG locktag; + + if (GetAnyGrantedHeavyweightLock(&locktag)) + { + StringInfoData locktagbuf; + + initStringInfo(&locktagbuf); + DescribeLockTag(&locktagbuf, &locktag); + + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot wait for a standby LSN while holding locks"), + errdetail("This session holds a lock on %s, which could make recovery wait for this session while this session waits for recovery.", + locktagbuf.data), + 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 8d246ed5a4e..ce1f8cdef1c 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -673,6 +673,38 @@ LockHeldByMe(const LOCKTAG *locktag, return false; } +/* + * GetAnyGrantedHeavyweightLock -- find some heavyweight lock this backend owns + * + * Returns true and stores the tag of an arbitrary granted lock in *locktag if + * the local lock table represents any lock we own, false otherwise. Which + * lock is reported is unspecified when we hold several; callers use it only to + * give the user a starting point. + * + * A LOCALLOCK entry can remain after an unsuccessful lock acquisition, so only + * entries with a positive local hold count represent locks we own. + */ +bool +GetAnyGrantedHeavyweightLock(LOCKTAG *locktag) +{ + HASH_SEQ_STATUS status; + LOCALLOCK *locallock; + + hash_seq_init(&status, LockMethodLocalHash); + + while ((locallock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + if (locallock->nLocks > 0) + { + *locktag = locallock->tag.lock; + 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..c63c9075466 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 GetAnyGrantedHeavyweightLock(LOCKTAG *locktag); #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..1ba79951b80 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -431,6 +431,42 @@ $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 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 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 locks/, + "reject replay wait while holding an advisory lock"); + +# A wait whose target has already been reached returns without sleeping, so it +# cannot join a cycle and stays allowed regardless of the locks held. +my $reached = $node_standby->safe_psql( + 'postgres', qq[ + BEGIN; + LOCK TABLE wait_test IN ACCESS SHARE MODE; + WAIT FOR LSN '${lsn1}';]); +is($reached, 'success', + "allow an already-reached wait while holding a relation 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