diff --git a/doc/src/sgml/ref/wait.sgml b/doc/src/sgml/ref/wait.sgml index cddcbf09695..2917309c0d5 100644 --- a/doc/src/sgml/ref/wait.sgml +++ b/doc/src/sgml/ref/wait.sgml @@ -260,10 +260,16 @@ WAIT FOR LSN 'lsn' WAIT must be executed as a top-level command. It cannot be executed from a function, procedure, or - DO block. It also requires that no active or - registered snapshot be held, and therefore cannot be used in contexts - where such a snapshot must remain active, including transactions running - at isolation levels higher than READ COMMITTED. + DO block. It also cannot be executed while the + current transaction holds a snapshot. WAIT itself + acquires none, so it can run before the first snapshot-taking statement + of a REPEATABLE READ or SERIALIZABLE + transaction, but not after it, and not while a cursor or an exported + snapshot holds one at any isolation level. A snapshot held here could + delay replay, which standby_replay waits for and which + other standby modes can end up waiting for too. That is also why + WAIT is a command rather than a function or a + procedure, which execute with one held. diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c index 2223e91a3e3..86cb27ea2e1 100644 --- a/src/backend/commands/wait.c +++ b/src/backend/commands/wait.c @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "access/xlog.h" #include "access/xlogrecovery.h" #include "access/xlogwait.h" @@ -155,8 +156,9 @@ ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel, if (HaveRegisteredOrActiveSnapshot()) ereport(ERROR, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("WAIT must be called without an active or registered snapshot"), - errdetail("WAIT cannot be executed within a transaction with an isolation level higher than READ COMMITTED.")); + errmsg("WAIT cannot be executed while the current transaction holds a snapshot"), + IsolationUsesXactSnapshot() ? + errdetail("This transaction runs at an isolation level higher than READ COMMITTED, so it holds a snapshot from its first query until it ends.") : 0); /* * As the result we should hold no snapshot, and correspondingly our xmin diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl index 3bc936e0f91..0fe5b9c6f67 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -282,13 +282,38 @@ $node_standby->psql( ok($stderr =~ /recovery is in progress/, "get an error when running primary_flush on the standby"); +# A transaction-snapshot-mode transaction has no snapshot until its first +# query, so WAIT FOR is allowed before that point. The target is already +# replayed here, so the command returns immediately. +$output = $node_standby->safe_psql('postgres', + "BEGIN ISOLATION LEVEL REPEATABLE READ; WAIT FOR LSN '${lsn2}'; COMMIT;"); +ok($output eq "success", + "WAIT FOR is allowed before the transaction snapshot is taken"); + $node_standby->psql( 'postgres', "BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; WAIT FOR LSN '${lsn3}';", stderr => \$stderr); -ok( $stderr =~ /WAIT must be called without an active or registered snapshot/, +ok( $stderr =~ + /WAIT cannot be executed while the current transaction holds a snapshot/, "get an error when running in a transaction with an isolation level higher than REPEATABLE READ" ); +ok( $stderr =~ + /isolation level higher than READ COMMITTED, so it holds a snapshot/, + "the isolation level is given as the reason when it is the reason"); + +# The same error at READ COMMITTED must not blame the isolation level. +$node_standby->psql( + 'postgres', + "BEGIN; DECLARE c CURSOR FOR SELECT 1; WAIT FOR LSN '${lsn3}';", + stderr => \$stderr); +ok( $stderr =~ + /WAIT cannot be executed while the current transaction holds a snapshot/, + "get an error when a cursor holds a snapshot"); +unlike( + $stderr, + qr/isolation level/, + "the isolation level is not blamed at READ COMMITTED"); # Test wrapping WAIT FOR into function, procedure, and anonymous DO block -- # should error