From 39ee9cd7e774917d2c18630ed40ea55cd9bc3f47 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 17 Sep 2026 01:54:09 +0300 Subject: [PATCH v9] Say what WAIT's snapshot restriction actually is The reference page said WAIT "cannot be used in contexts where such a snapshot must remain active, including transactions running at isolation levels higher than READ COMMITTED", which reads as a blanket ban at those levels. It is not one. The rule the code enforces is that the current transaction must hold no snapshot, and a transaction-snapshot-mode transaction has no snapshot until its first query: WaitStmt is exempt in PlannedStmtRequiresSnapshot(), so the command itself does not take one. WAIT therefore works before that first query, which is the order an application wants anyway, and it is what 447aae13b03 set out to allow by keeping the wait snapshot-free. State the rule in terms of the snapshot rather than the isolation level, say that WAIT acquires none of its own and name the other ways to hold one, and give the consequence of holding one: it can delay replay, which standby_replay waits for and which the other standby modes ultimately depend on as well. The error for a held snapshot named the internal snapshot states instead of saying what the user did, and attached the isolation-level explanation as DETAIL unconditionally, so a READ COMMITTED session with a cursor was told its isolation level was too high. Report what is held rather than how it is held, and emit the DETAIL only when the isolation level is in fact the reason. Reported-by: Sami Imseih Reported-by: Xuneng Zhou Reviewed-by: Sami Imseih Reviewed-by: Xuneng Zhou Discussion: https://postgr.es/m/CAN12%2BYJddgAZzA36JZFX%2BYrnNR7Uubp9qWFz6ASs3v0kL30AMA%40mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/ref/wait.sgml | 14 ++++++++++---- src/backend/commands/wait.c | 6 ++++-- src/test/recovery/t/049_wait_for_lsn.pl | 23 ++++++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) 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..e9b1dcfc810 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -282,13 +282,34 @@ $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 -- 2.55.0