On Fri, Jul 31, 2026 at 1:02 PM Xuneng Zhou <[email protected]> wrote: > > On Fri, Jul 31, 2026 at 11:39 AM Xuneng Zhou <[email protected]> wrote: > > > > On Fri, Jul 31, 2026 at 9:46 AM Xuneng Zhou <[email protected]> wrote: > > > > > > On Fri, Jul 31, 2026 at 8:58 AM Xuneng Zhou <[email protected]> wrote: > > > > > > > > On Thu, Jul 30, 2026 at 10:51 AM Xuneng Zhou <[email protected]> > > > > wrote: > > > > > > > > > > On Tue, Jul 28, 2026 at 9:25 PM Xuneng Zhou <[email protected]> > > > > > wrote: > > > > > > > > > > > > On Mon, Jul 27, 2026 at 7:03 PM Xuneng Zhou <[email protected]> > > > > > > wrote: > > > > > > > > > > > > > > Hi Alexander, > > > > > > > > > > > > > > On Thu, Jul 9, 2026 at 8:24 PM Xuneng Zhou <[email protected]> > > > > > > > wrote: > > > > > > > > > > > > > > > > Hi! > > > > > > > > > > > > > > > > As Noah pointed out in [1], auxiliary processes are supported > > > > > > > > in the > > > > > > > > facility, but proper cleanup is absent when they exit. I have > > > > > > > > attached > > > > > > > > a patch trying to address that as suggested before getting > > > > > > > > sidetracked > > > > > > > > for too long with the issue raised in [2]. > > > > > > > > > > > > > > I'd like to propose a series of patches to do some further > > > > > > > clean-ups & > > > > > > > enhancements. The first few are mostly mechanical, > > > > > > > straightforward, > > > > > > > and the last two are more subtle. I appreciate your > > > > > > > inputs/thoughts on > > > > > > > them. > > > > > > > > > > > > > > 1) Patch 1 to address the issue raised by Noah. > > > > > > > > > > > > > > 2) Patch 2 to fix two small docs mismatches. > > > > > > > > > > > > > > 3) Patch 3 to optimize the WaitLSNLock acquisition in > > > > > > > deleteLSNWaiter. > > > > > > > > > > > > > > 4) Patch 4 to clarify lsn waiter cleanup after wakeup. > > > > > > > > > > > > > > 5) Patch 5 to address a rare issue of deregistration of waiters > > > > > > > whose > > > > > > > target lsn has reached but then flashed back. > > > > > > > > > > > > > > Currently, the waiters are removed from the heap by the wakers if > > > > > > > their target lsn has reached. We offer no re-registration service > > > > > > > in > > > > > > > the reception desk for them assuming they won't need to extend > > > > > > > their > > > > > > > stay. This assumption holds if the lsn is monotonically > > > > > > > increasing, > > > > > > > which is true for most cases. However, there seems to be a catch > > > > > > > for > > > > > > > the wal write/flush lsn from the wal receiver side. In the cases > > > > > > > of > > > > > > > restarting wal receiver, the write/flush lsn could be reset to > > > > > > > positions lagging behind(the attached restarting cases file > > > > > > > summarized > > > > > > > by Sol with inputs from me shows a matrix of this). > > > > > > > > > > > > > > One problematic interleaving, for reset cases 2–4 in the attached > > > > > > > file, is the following: > > > > > > > > > > > > > > Let W be the old published write position, X the waiter’s target, > > > > > > > S > > > > > > > the new streaming segment start, and R the replay position, with: > > > > > > > max(N, R) < X ≤ W > > > > > > > > > > > > > > [T0] A backend registers a standby_write waiter for target X and > > > > > > > sleeps. > > > > > > > > > > > > > > [T1] The walreceiver publishes writtenUpto = W and calls > > > > > > > WaitLSNWakeup(W). Because X ≤ W, the waker removes the waiter. > > > > > > > from > > > > > > > the heap, sets inHeap = false, and sets its latch. > > > > > > > > > > > > > > [T2] Before the waiter runs, streaming is restarted. > > > > > > > RequestXLogStreaming() resets shared writtenUpto from W to S. The > > > > > > > effective standby_write position is now: max(writtenUpto, replay) > > > > > > > = > > > > > > > max(S, R) = R. Therefore the effective position has regressed > > > > > > > below X. > > > > > > > > > > > > > > [T3] The waiter consumes the old latch notification and rechecks > > > > > > > its > > > > > > > condition. It observes R < X. In the pre-fix code, it goes back to > > > > > > > sleep without re-registering, even though it is no longer in the > > > > > > > waiters heap. > > > > > > > > > > > > I took a second look at the wording. I had asked Sol to polish this > > > > > > part for clarity, but the revised version is inaccurate. It should > > > > > > be: > > > > > > > > > > > > [T2] Before the waiter runs, streaming is restarted. > > > > > > RequestXLogStreaming() resets writtenUpto from W to S. The effective > > > > > > standby_write position becomes max(S, R), which is below X. > > > > > > > > > > > > [T3] The waiter consumes the earlier latch notification and rechecks > > > > > > its condition. It observes that max(S, R) < X. In the pre-fix code, > > > > > > it > > > > > > waits again without re-registering, even though the waker has > > > > > > already > > > > > > removed it from the heap. > > > > > > > > > > > > Sorry for not double-checking it. > > > > > > > > > > > > > [T4] The walreceiver later receives and publishes WAL through X > > > > > > > and > > > > > > > calls WaitLSNWakeup() again. The waiter is absent from the heap, > > > > > > > so it > > > > > > > receives no notification and can remain asleep even though its > > > > > > > target > > > > > > > has now been reached. > > > > > > > > > > > > > > One possible fix for the issue that I have considered is to make > > > > > > > write/flush lsn monotonic so it won't flash back once advanced. > > > > > > > This > > > > > > > seems to work well at first glance. However, it has two > > > > > > > shortcomings: > > > > > > > one is that it can blur the definition of write lsn and another is > > > > > > > that it won't fix the cross-timeline lsn regression. Currently, > > > > > > > writtenUpto is the current walreceiver stream’s write frontier. > > > > > > > Giving > > > > > > > that value monotonicity would turn it into the greatest numeric > > > > > > > lsn > > > > > > > ever written by any receiver incarnation. Cross-timeline lsn > > > > > > > flashback > > > > > > > is expected given the fork point is smaller than the pre-restart > > > > > > > lsns. > > > > > > > Extending it across timelines would incorrectly associate > > > > > > > progress on > > > > > > > an old timeline with the new timeline. The patch places the fix > > > > > > > in the > > > > > > > waiter side -- let the backend do a recheck and re-registration > > > > > > > if the > > > > > > > published lsn is regressed. > > > > > > > > > > > > > > Another aspect is the test, it's relatively simple to construct > > > > > > > the > > > > > > > failure case, but seems hard to do so deterministically. I spent > > > > > > > a lot > > > > > > > of time(maybe too much for an unconfirmed rare issue) trying to > > > > > > > tame > > > > > > > it in a simple way; it seems hard to orchestrate the startup > > > > > > > process, > > > > > > > wal receiver, waiting backend into above-like interleavings > > > > > > > end-to-end > > > > > > > without using complex synchronization like three injection > > > > > > > points. The > > > > > > > tricky part is to ensure max(N, R) < X ≤ W. We cannot stop the > > > > > > > startup > > > > > > > then the wal receiver to make sure that because we need the > > > > > > > former to > > > > > > > manipulate the later one. The current workaround is to add a > > > > > > > synthetic > > > > > > > helper for removing the heap node even if its target lsn has not > > > > > > > actually reached, then trigger the real wake-up call by advancing > > > > > > > the > > > > > > > published lsn to check whether the waiter is notified and the wait > > > > > > > completes. The rationale behind this is that the premature removal > > > > > > > from the heap without re-registration is the underlying issue > > > > > > > regardless of the specific lsn types and failure scenarios. That > > > > > > > said, > > > > > > > I am unsure whether this is a proper way or better alternatives > > > > > > > exist. > > > > > > > > > > > > > > 6) Patch 6 to cover a missing wake-up point for primary-flush > > > > > > > waiters [WIP] > > > > > > > > > > After learning more of the test/modules, I became less convinced to > > > > > use an injection point for the proof of re-registration in the test > > > > > which seems somewhat hacky to me. It works in a non-obvious manner -- > > > > > we infer from the position of the code that the registration occurs, > > > > > but it is prone to future changes unless we add more comments to > > > > > explain its usage. Just wondering whether it makes sense to add a > > > > > helper to probe & prove the waiter is registered in the heap. > > > > > > > > > > > > > Updated the patch 5 according to the methodology. Added a new test > > > > helper, test_wait_lsn_waiter_is_registered(), that checks whether a > > > > process is registered in the waiters heap for a specified target LSN, > > > > wait type, and PID. > > > > > > Generalized the test_wait_lsn_wakeup helper to specify the wait type > > > to pair with test_wait_lsn_waiter_is_registered. > > > > V4 improved some commit messages and comments of patch 1-4. > > There are two inaccurate expressions in the comments of patch 1 and 4. > They are now fixed in v5. Sorry for the noise here.
Here's v6 to update the comments just like the two immediate precedents did. It seems annoying to do/say so. Should've shipped a self-settled version. Sorry for haven't done otherwise... -- Regards, Xuneng Zhou HighGo Software Co., Ltd.
From 742e533e1d22eb1c84318df071806ffd447d1eb3 Mon Sep 17 00:00:00 2001 From: alterego655 <[email protected]> Date: Fri, 24 Jul 2026 16:05:35 +0800 Subject: [PATCH v6 2/5] Fix WAIT FOR LSN documentation examples Pad example LSNs to match pg_lsn_out() output, and use "standby_replay LSN" in the timeout error to match the server message. --- doc/src/sgml/ref/wait_for.sgml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/ref/wait_for.sgml b/doc/src/sgml/ref/wait_for.sgml index cd5dd031991..01dc2a84a1a 100644 --- a/doc/src/sgml/ref/wait_for.sgml +++ b/doc/src/sgml/ref/wait_for.sgml @@ -301,7 +301,7 @@ UPDATE 100 postgres=# SELECT pg_current_wal_insert_lsn(); pg_current_wal_insert_lsn --------------------------- - 0/306EE20 + 0/0306EE20 (1 row) </programlisting> @@ -310,7 +310,7 @@ postgres=# SELECT pg_current_wal_insert_lsn(); changes made on primary should be guaranteed to be visible on replica. <programlisting> -postgres=# WAIT FOR LSN '0/306EE20'; +postgres=# WAIT FOR LSN '0/0306EE20'; status --------- success @@ -326,7 +326,7 @@ postgres=# SELECT * FROM movie WHERE genre = 'Drama'; Wait for flush (data durable on replica): <programlisting> -postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_flush'); +postgres=# WAIT FOR LSN '0/0306EE20' WITH (MODE 'standby_flush'); status --------- success @@ -338,7 +338,7 @@ postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_flush'); Wait for write with timeout: <programlisting> -postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_write', TIMEOUT '100ms', NO_THROW); +postgres=# WAIT FOR LSN '0/0306EE20' WITH (MODE 'standby_write', TIMEOUT '100ms', NO_THROW); status --------- success @@ -350,7 +350,7 @@ postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_write', TIMEOUT '100ms', Wait for flush on primary: <programlisting> -postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'primary_flush'); +postgres=# WAIT FOR LSN '0/0306EE20' WITH (MODE 'primary_flush'); status --------- success @@ -362,8 +362,8 @@ postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'primary_flush'); If the target LSN is not reached before the timeout, an error is thrown: <programlisting> -postgres=# WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '0.1s'); -ERROR: timed out while waiting for target LSN 0/306EE20 to be replayed; current replay LSN 0/306EA60 +postgres=# WAIT FOR LSN '0/0306EE20' WITH (TIMEOUT '0.1s'); +ERROR: timed out while waiting for target LSN 0/0306EE20 to be replayed; current standby_replay LSN 0/0306EA60 </programlisting> </para> @@ -372,7 +372,7 @@ ERROR: timed out while waiting for target LSN 0/306EE20 to be replayed; current <parameter>NO_THROW</parameter> option: <programlisting> -postgres=# WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '100ms', NO_THROW); +postgres=# WAIT FOR LSN '0/0306EE20' WITH (TIMEOUT '100ms', NO_THROW); status --------- timeout -- 2.51.0
From ac5f7ebb24a021a69a851e31d860af89591424db Mon Sep 17 00:00:00 2001 From: alterego655 <[email protected]> Date: Fri, 24 Jul 2026 16:37:20 +0800 Subject: [PATCH v6 4/5] Clarify LSN waiter cleanup after wakeup WaitLSNWakeup() can be called by several processes, not only the startup process. Update the cleanup comment to explain that another process may remove the waiter before waking it and that inHeap prevents double deletion. --- src/backend/access/transam/xlogwait.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c index 6b501fef7ba..9e05505344f 100644 --- a/src/backend/access/transam/xlogwait.c +++ b/src/backend/access/transam/xlogwait.c @@ -526,9 +526,10 @@ WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) } /* - * Delete our process from the shared memory heap. We might already be - * deleted by the startup process. The 'inHeap' flags prevents us from - * the double deletion. + * A progress waker, such as the startup process during WAL replay, may + * already have removed this waiter through WaitLSNWakeup() before setting + * its latch. The inHeap flag makes this cleanup safe whether or not the + * entry remains in the heap. */ deleteLSNWaiter(lsnType); -- 2.51.0
From b48718e36bbb93b887cb3a7e1ce7d9606466c6eb Mon Sep 17 00:00:00 2001 From: alterego655 <[email protected]> Date: Fri, 24 Jul 2026 15:44:16 +0800 Subject: [PATCH v6 1/5] Add wait-for-lsn process-exit cleanup callback Register an on_shmem_exit callback lazily before a process enters a wait-for-lsn heap, so stale wait state is removed if the process exits while waiting. This keeps cleanup local to xlogwait.c and covers non-backend callers as well. --- src/backend/access/transam/xlogwait.c | 38 +++++++++++++++++++++++++++ src/backend/storage/lmgr/proc.c | 6 ----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c index 582dde3b061..85de3f5cdae 100644 --- a/src/backend/access/transam/xlogwait.c +++ b/src/backend/access/transam/xlogwait.c @@ -54,6 +54,7 @@ #include "miscadmin.h" #include "pgstat.h" #include "replication/walreceiver.h" +#include "storage/ipc.h" #include "storage/latch.h" #include "storage/proc.h" #include "storage/shmem.h" @@ -69,8 +70,12 @@ static int waitlsn_cmp(const pairingheap_node *a, const pairingheap_node *b, struct WaitLSNState *waitLSNState = NULL; +static bool waitLSNShmemExitRegistered = false; + static void WaitLSNShmemRequest(void *arg); static void WaitLSNShmemInit(void *arg); +static void WaitLSNShmemExit(int code, Datum arg); +static void RegisterWaitLSNShmemExit(void); const ShmemCallbacks WaitLSNShmemCallbacks = { .request_fn = WaitLSNShmemRequest, @@ -378,6 +383,31 @@ WaitLSNCleanup(void) } } +/* + * Exit callback to clean up any LSN wait state left behind if this process + * exits while waiting. Transaction abort paths call WaitLSNCleanup() + * directly. + */ +static void +WaitLSNShmemExit(int code, Datum arg) +{ + WaitLSNCleanup(); +} + +/* + * Register shared-memory exit cleanup once per process. A backend may + * execute WAIT FOR LSN more than once. + */ +static void +RegisterWaitLSNShmemExit(void) +{ + if (!waitLSNShmemExitRegistered) + { + on_shmem_exit(WaitLSNShmemExit, 0); + waitLSNShmemExitRegistered = true; + } +} + /* * Check if the given LSN type requires recovery to be in progress. * Standby wait types (replay, write, flush) require recovery; @@ -412,6 +442,14 @@ WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) /* Should have a valid proc number */ Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends + NUM_AUXILIARY_PROCS); + /* + * Ensure cleanup is registered before publishing our waiter entry. + * on_shmem_exit callbacks run in reverse registration order, so this + * callback runs before the earlier-registered ProcKill() and removes the + * entry before our PGPROC slot can be reused. + */ + RegisterWaitLSNShmemExit(); + if (timeout > 0) { endtime = TimestampTzPlusMilliseconds(GetCurrentTimestamp(), timeout); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 9d6e69175a5..624cecc6bc5 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -37,7 +37,6 @@ #include "access/transam.h" #include "access/twophase.h" #include "access/xlogutils.h" -#include "access/xlogwait.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" @@ -965,11 +964,6 @@ ProcKill(int code, Datum arg) */ LWLockReleaseAll(); - /* - * Cleanup waiting for LSN if any. - */ - WaitLSNCleanup(); - /* Cancel any pending condition variable sleep, too */ ConditionVariableCancelSleep(); -- 2.51.0
From 6ed8533ace2aff0eb5bf0a3eeefe96af23839ba5 Mon Sep 17 00:00:00 2001 From: alterego655 <[email protected]> Date: Fri, 24 Jul 2026 16:34:29 +0800 Subject: [PATCH v6 3/5] Avoid locking when an LSN waiter is already removed WaitLSNWakeup() removes each selected waiter from its heap and clears its inHeap flag before setting its latch. When such a waiter later calls deleteLSNWaiter(), it acquires WaitLSNLock exclusively only to discover that there is nothing left to remove. Waking many waiters can therefore make them serialize on the lock for no useful work. Check inHeap before acquiring WaitLSNLock. A lockless false value is conclusive because only the owning backend can change inHeap from false to true. A concurrent waker can only clear it. A stale true value falls through to the existing recheck under the lock. --- src/backend/access/transam/xlogwait.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c index 85de3f5cdae..6b501fef7ba 100644 --- a/src/backend/access/transam/xlogwait.c +++ b/src/backend/access/transam/xlogwait.c @@ -252,6 +252,15 @@ deleteLSNWaiter(WaitLSNType lsnType) Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + /* + * Avoid taking WaitLSNLock if a waker has already removed us. Only this + * backend can set inHeap; other processes can only clear it. Therefore + * false is conclusive, while a stale true is harmless because it is + * rechecked under WaitLSNLock below. + */ + if (!procInfo->inHeap) + return; + LWLockAcquire(WaitLSNLock, LW_EXCLUSIVE); Assert(procInfo->lsnType == lsnType); -- 2.51.0
From 02dec47758850e8db53a96b090f8822ccbfb0c0b Mon Sep 17 00:00:00 2001 From: alterego655 <[email protected]> Date: Mon, 27 Jul 2026 18:02:45 +0800 Subject: [PATCH v6 5/5] Re-register LSN waiters after stale wakeups WaitLSNWakeup() removes a waiter from the heap before setting its latch. If the position that caused the wakeup moves backwards before the waiter rechecks it, as can happen when WAL streaming restarts, the waiter may sleep again while no longer registered. Subsequent WAL progress then cannot wake it. When an unmet waiter finds that it is no longer in the heap, add it back and restart the loop. Rereading the position after registration also prevents missing an advance between the previous read and the re-add. Add deterministic TAP coverage that simulates a stale standby_write wakeup without advancing the actual write or replay positions. --- src/backend/access/transam/xlogwait.c | 20 ++++ src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_wait_lsn/Makefile | 21 ++++ src/test/modules/test_wait_lsn/meson.build | 22 +++++ .../test_wait_lsn/test_wait_lsn--1.0.sql | 14 +++ .../modules/test_wait_lsn/test_wait_lsn.c | 99 +++++++++++++++++++ .../test_wait_lsn/test_wait_lsn.control | 4 + src/test/recovery/Makefile | 3 +- src/test/recovery/t/049_wait_for_lsn.pl | 85 ++++++++++++++++ 10 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/test_wait_lsn/Makefile create mode 100644 src/test/modules/test_wait_lsn/meson.build create mode 100644 src/test/modules/test_wait_lsn/test_wait_lsn--1.0.sql create mode 100644 src/test/modules/test_wait_lsn/test_wait_lsn.c create mode 100644 src/test/modules/test_wait_lsn/test_wait_lsn.control diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c index 9e05505344f..ea1d2afba27 100644 --- a/src/backend/access/transam/xlogwait.c +++ b/src/backend/access/transam/xlogwait.c @@ -442,6 +442,7 @@ WaitLSNResult WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) { XLogRecPtr currentLSN; + WaitLSNProcInfo *procInfo; TimestampTz endtime = 0; int wake_events = WL_LATCH_SET | WL_POSTMASTER_DEATH; @@ -451,6 +452,8 @@ WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) /* Should have a valid proc number */ Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends + NUM_AUXILIARY_PROCS); + procInfo = &waitLSNState->procInfos[MyProcNumber]; + /* * Ensure cleanup is registered before publishing our waiter entry. * on_shmem_exit callbacks run in reverse registration order, so this @@ -500,6 +503,23 @@ WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) break; } + /* + * The target is not reached. Normally we remain in the waiters heap + * and can sleep again. A wakeup can become stale, however, if the + * position moves backwards after the waker removes us (for example, + * after a walreceiver restart). Re-register in that case and reread + * the position, since an advance before the re-add could not wake us. + * + * It is safe to read inHeap without the lock because only this process + * sets it true. If a waker clears it concurrently, it also sets our + * latch, so we will recheck and re-register if necessary. + */ + if (!procInfo->inHeap) + { + addLSNWaiter(targetLSN, lsnType); + continue; + } + if (timeout > 0) { delay_ms = TimestampDifferenceMilliseconds(GetCurrentTimestamp(), endtime); diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 098bb8142ae..bb88b3058ed 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -53,6 +53,7 @@ SUBDIRS = \ test_shm_mq \ test_slru \ test_tidstore \ + test_wait_lsn \ unsafe_tests \ worker_spi \ xid_wraparound diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 4bca42bb370..ce09e00531d 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -54,6 +54,7 @@ subdir('test_shmem') subdir('test_shm_mq') subdir('test_slru') subdir('test_tidstore') +subdir('test_wait_lsn') subdir('typcache') subdir('unsafe_tests') subdir('worker_spi') diff --git a/src/test/modules/test_wait_lsn/Makefile b/src/test/modules/test_wait_lsn/Makefile new file mode 100644 index 00000000000..e9ce2fd3ad0 --- /dev/null +++ b/src/test/modules/test_wait_lsn/Makefile @@ -0,0 +1,21 @@ +# src/test/modules/test_wait_lsn/Makefile + +MODULE_big = test_wait_lsn +OBJS = \ + $(WIN32RES) \ + test_wait_lsn.o +PGFILEDESC = "test_wait_lsn - test code for WAIT FOR LSN" + +EXTENSION = test_wait_lsn +DATA = test_wait_lsn--1.0.sql + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_wait_lsn +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_wait_lsn/meson.build b/src/test/modules/test_wait_lsn/meson.build new file mode 100644 index 00000000000..66e074298d7 --- /dev/null +++ b/src/test/modules/test_wait_lsn/meson.build @@ -0,0 +1,22 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_wait_lsn_sources = files( + 'test_wait_lsn.c', +) + +if host_system == 'windows' + test_wait_lsn_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_wait_lsn', + '--FILEDESC', 'test_wait_lsn - test code for WAIT FOR LSN',]) +endif + +test_wait_lsn = shared_module('test_wait_lsn', + test_wait_lsn_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_wait_lsn + +test_install_data += files( + 'test_wait_lsn.control', + 'test_wait_lsn--1.0.sql', +) diff --git a/src/test/modules/test_wait_lsn/test_wait_lsn--1.0.sql b/src/test/modules/test_wait_lsn/test_wait_lsn--1.0.sql new file mode 100644 index 00000000000..4111223726f --- /dev/null +++ b/src/test/modules/test_wait_lsn/test_wait_lsn--1.0.sql @@ -0,0 +1,14 @@ +/* src/test/modules/test_wait_lsn/test_wait_lsn--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_wait_lsn" to load this file. \quit + +CREATE FUNCTION test_wait_lsn_wakeup( + pg_catalog.text, pg_catalog.pg_lsn) +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_wait_lsn_waiter_is_registered( + pg_catalog.int4, pg_catalog.text, pg_catalog.pg_lsn) +RETURNS pg_catalog.bool STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_wait_lsn/test_wait_lsn.c b/src/test/modules/test_wait_lsn/test_wait_lsn.c new file mode 100644 index 00000000000..1eebcf61c5f --- /dev/null +++ b/src/test/modules/test_wait_lsn/test_wait_lsn.c @@ -0,0 +1,99 @@ +/*-------------------------------------------------------------------------- + * + * test_wait_lsn.c + * Test support for WAIT FOR LSN. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_wait_lsn/test_wait_lsn.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xlogwait.h" +#include "fmgr.h" +#include "storage/lwlock.h" +#include "storage/proc.h" +#include "storage/procarray.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_wait_lsn_wakeup); +PG_FUNCTION_INFO_V1(test_wait_lsn_waiter_is_registered); + +static WaitLSNType +parse_wait_lsn_type(text *mode_text) +{ + char *mode = text_to_cstring(mode_text); + WaitLSNType lsn_type; + + if (pg_strcasecmp(mode, "standby_replay") == 0) + lsn_type = WAIT_LSN_TYPE_STANDBY_REPLAY; + else if (pg_strcasecmp(mode, "standby_write") == 0) + lsn_type = WAIT_LSN_TYPE_STANDBY_WRITE; + else if (pg_strcasecmp(mode, "standby_flush") == 0) + lsn_type = WAIT_LSN_TYPE_STANDBY_FLUSH; + else if (pg_strcasecmp(mode, "primary_flush") == 0) + lsn_type = WAIT_LSN_TYPE_PRIMARY_FLUSH; + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized WAIT FOR LSN mode \"%s\"", mode))); + + pfree(mode); + return lsn_type; +} + +/* + * Wake all waiters of the supplied type through the supplied LSN without + * advancing the underlying WAL position. + */ +Datum +test_wait_lsn_wakeup(PG_FUNCTION_ARGS) +{ + WaitLSNType lsn_type = parse_wait_lsn_type(PG_GETARG_TEXT_PP(0)); + XLogRecPtr upto_lsn = PG_GETARG_LSN(1); + + WaitLSNWakeup(lsn_type, upto_lsn); + + PG_RETURN_VOID(); +} + +/* + * Check whether the backend with the supplied PID is registered for the + * supplied mode and target. ProcArrayLock stabilizes the PID mapping, while + * WaitLSNLock protects the registration state. + */ +Datum +test_wait_lsn_waiter_is_registered(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + WaitLSNType lsn_type = parse_wait_lsn_type(PG_GETARG_TEXT_PP(1)); + XLogRecPtr target_lsn = PG_GETARG_LSN(2); + bool registered = false; + PGPROC *proc; + + LWLockAcquire(ProcArrayLock, LW_SHARED); + proc = BackendPidGetProcWithLock(pid); + + if (proc != NULL) + { + ProcNumber procno = GetNumberFromPGProc(proc); + WaitLSNProcInfo *proc_info = &waitLSNState->procInfos[procno]; + + LWLockAcquire(WaitLSNLock, LW_SHARED); + registered = proc_info->inHeap && + proc_info->procno == procno && + proc_info->lsnType == lsn_type && + proc_info->waitLSN == target_lsn; + LWLockRelease(WaitLSNLock); + } + + LWLockRelease(ProcArrayLock); + + PG_RETURN_BOOL(registered); +} diff --git a/src/test/modules/test_wait_lsn/test_wait_lsn.control b/src/test/modules/test_wait_lsn/test_wait_lsn.control new file mode 100644 index 00000000000..7b84150e179 --- /dev/null +++ b/src/test/modules/test_wait_lsn/test_wait_lsn.control @@ -0,0 +1,4 @@ +comment = 'Test code for WAIT FOR LSN' +default_version = '1.0' +module_pathname = '$libdir/test_wait_lsn' +relocatable = true diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile index d41aaaf8ae1..9c4102b6b2c 100644 --- a/src/test/recovery/Makefile +++ b/src/test/recovery/Makefile @@ -12,7 +12,8 @@ EXTRA_INSTALL=contrib/pg_prewarm \ contrib/pg_stat_statements \ contrib/test_decoding \ - src/test/modules/injection_points + src/test/modules/injection_points \ + src/test/modules/test_wait_lsn subdir = src/test/recovery top_builddir = ../../.. diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl index bc216064714..cb7d4d461de 100644 --- a/src/test/recovery/t/049_wait_for_lsn.pl +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -1055,6 +1055,91 @@ is($boundary_session->{stdout}, 'success', "standby_replay: waiter at current + 1 wakes when replay advances"); +# 11d. A standby_write waiter removed from the waiters heap by a stale wakeup +# must re-register if its target has not actually been reached. +SKIP: +{ + skip 'Required test extension is not installed', 2 + unless $rcv_primary->check_extension('test_wait_lsn'); + + $rcv_primary->safe_psql('postgres', 'CREATE EXTENSION test_wait_lsn'); + $rcv_primary->wait_for_catchup($rcv_standby); + + # Stop streaming before generating the target. This keeps both the + # walreceiver write position and the replay position below the target. + stop_walreceiver($rcv_standby); + $rcv_primary->safe_psql('postgres', 'INSERT INTO rcv_test VALUES (300)'); + my $stale_target = $rcv_primary->safe_psql('postgres', + 'SELECT pg_current_wal_insert_lsn()'); + + # Keep WAIT FOR untimed. After streaming resumes below, its own timeout + # could wake the backend and make the completion check pass even if the + # WAL-progress wakeup were lost. + my $stale_waiter_name = 'wait_for_lsn_stale_wakeup'; + my $stale_session = $rcv_standby->background_psql('postgres', + connstr => $rcv_standby->connstr('postgres') + . " application_name=$stale_waiter_name"); + + $stale_session->set_query_timer_restart(); + + my $stale_waiter_pid = $rcv_standby->safe_psql( + 'postgres', + "SELECT pid FROM pg_stat_activity + WHERE application_name = '$stale_waiter_name'"); + die "could not determine stale waiter PID: '$stale_waiter_pid'" + unless $stale_waiter_pid =~ /^[0-9]+$/; + + $stale_session->query_until( + qr/started/, qq[ + \\echo started + WAIT FOR LSN '$stale_target' WITH (MODE 'standby_write'); + \\echo completed + ]); + + $rcv_standby->poll_query_until( + 'postgres', qq[ + SELECT test_wait_lsn_waiter_is_registered( + $stale_waiter_pid, 'standby_write', '$stale_target') + ] + ) or die "standby_write waiter did not register"; + + # The write position is below the target because the receiver was stopped + # before the target was generated. Verify that replay is below it too. + $rcv_standby->safe_psql( + 'postgres', qq[ + SELECT pg_wal_lsn_diff( + '$stale_target'::pg_lsn, pg_last_wal_replay_lsn()) > 0 + ]) eq 't' + or die "standby replay reached the target before the stale wakeup"; + + # Simulate the state left by a wakeup that became stale before the waiter + # could recheck its target. The helper removes the waiter from the heap + # and wakes it without changing the actual write or replay positions. To + # the waiter, this is indistinguishable from the position reaching the + # target and then decreasing before the recheck. + $rcv_standby->safe_psql('postgres', + "SELECT test_wait_lsn_wakeup('standby_write', '$stale_target')"); + + # The position is still below the target, so the waiter must restore its + # heap registration before sleeping again. + ok( $rcv_standby->poll_query_until( + 'postgres', qq[ + SELECT test_wait_lsn_waiter_is_registered( + $stale_waiter_pid, 'standby_write', '$stale_target') + ]), + "standby_write waiter re-registers after a stale wakeup" + ) or die "standby_write waiter did not re-register"; + + # Reach the target for real; WAL progress should wake the re-registered waiter. + resume_walreceiver($rcv_standby); + + like( + $stale_session->query_until(qr/completed/, ''), + qr/^success\r?\ncompleted/m, + "standby_write waiter completes once the target is reached"); + $stale_session->quit; +} + $rcv_standby->stop; $rcv_primary->stop; -- 2.51.0
