pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedDeadlock detector fails to activate on a hot standby replica
43+ messages / 6 participants
[nested] [flat]
* Deadlock detector fails to activate on a hot standby replica
@ 2026-01-19 12:43 Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-01-19 12:43 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org
Dear Hackers,
The deadlock detection mechanism fails to activate when a deadlock occurs
between startup and backend processes on a hot standby replica, resulting in
unforeseen delays in the recovery. The deadlock may happen, when processing
XLOG_HEAP2_PRUNE_* messages. Automatic resolution of deadlocks remains
possible when reaching the specified max_standby_streaming_delay value, if it is
set. Sometimes this value is set to -1 which disables this timeout. This
issue appears consistently in versions 15 and later, when
log_startup_progress_interval was introduced.
The startup process notify the conflicting backend process to check for deadlocks
when deadlock_timeout is reached. It works in general, but doesn't work in some
scenarios. If to set deadlock_timeout to be greater than
log_startup_progress_interval, the deadlock detector will never be triggered,
but the startup process will wait for the the deadlock resolution until
max_standby_streaming_delay timeout is reached (if it is set).
It is reproducible with the attached tap test 900_startup_backend_deadlock.pl.
To reproduce, just copy this test into src/test/recovery/t and run it.
The problem seems to appear in timeout.c functionality, or in
ResolveRecoveryConflictWithBufferPin depending on how to understand the
semantics of the timeout api. The root cause - handle_sig_alarm (SIGALRM handler)
may be called when no active timeouts are reached. It sets the process latch
unconditionally, this, waking up the process.
The problem may be in an optimization when setitimer may not be called,
when the closest final time of active timeouts is greater than already set time.
The SIGARLM handler may be called when no active timeouts are reached.
Below is the scenatio when deadlock timeout is not activated:
(1) The startup process sets startup_progress_interval to 1000ms and continues
with the recovery of the received WAL.
(2) When processing XLOG_HEAP2_PRUNE_*, the startup process tries to lock the
buffer using LockBufferForCleanup that calls ResolveRecoveryConflictWithBufferPin.
The deadlock of startup and backend processes is possible (see
src/test/recovery/t/031_recovery_conflict.pl test). Image, we come to the deadlock.
(3) ResolveRecoveryConflictWithBufferPin sets deadlock timeout to 3000 ms and
waits for buffer pin to be unlocked or for the timeout using ProcWaitForSignal.
(4) When the startup process in ProcWaitForSignal, handle_sig_alarm is called
because startup_progress_interval is reached (the timeout was disabled, but
the real timer was not reset). It sets the process latch unconditionally and
reschedules timers - the current active timer will be rescheduled in ~2000 ms in
our case, if XLOG_HEAP2_PRUNE_ was received right after step (1).
It means, the next call of handle_sig_alarm will be in 2000 ms.
(5) ResolveRecoveryConflictWithBufferPin continues after ProcWaitForSignal,
disables all active timeouts and returns. LockBufferForCleanup sees that the
buffer is still locked and calls ResolveRecoveryConflictWithBufferPin again.
(6) ResolveRecoveryConflictWithBufferPin sets deadlock timeout to 3000 ms, but
the real timer is not changed - it will be triggered in 2000 ms. And, then,
wits for timeout in ProcWaitForSignal.
(7) The SIGALRM handler (handle_sig_alarm) is called in 2000 ms, it sets the
process latch, but the deadlock timeout is not yet reached. Once, it is not
reached, the startup process will not signal to the conflicting backend to check
for deadlocks. ResolveRecoveryConflictWithBufferPin resets all timeouts again
and transfer control to LockBufferForCleanup. The buffer is still locked, it
calls ResolveRecoveryConflictWithBufferPin again.
(8) And so on... The startup process will run forever. It will loop in
LockBufferForCleanup without any progress in recovery.
The problem is here - if an unforeseen SIGALRM is received before deadlock
timeout, it can lead to infinite loop in LockBufferForCleanup.
I see a couple of possible solutions:
1. Call seitimer every time when needed (see the demo patch [1]).
2. Redesign LockBufferForCleanup logic to support the cases when SIGALRM may
come unexpectedly.
3. Call SetLatch in handle_sig_alarm only if some timeout is reached.
The solution 1 is a simpler one, but it can not guarantee that some other
functionaly will set a timeout and will affect LockBufferForCleanup. The
solution 2 seems to be more robust, but it is harder to implement. Furthermore,
I can not exclude some other places, where the timeout functionality is used in
a wrong way. Solution 3 seems to be the simplest but there is an opinion, that
any SIGALRM should wake up the process (set the latch).
Any ideas?
[1] 900_startup_backend_deadlock.pl
[2] 0001-Fix-deadlock-detector-activation-in-startup-process.patch
Attachments:
900_startup_backend_deadlock.pl (application/x-perl, 4.4K) download
0001-Fix-deadlock-detector-activation-in-startup-process.patch (text/x-patch, 877B) download
From f50bdfc0beea8da43265eb279d3bc1e2a8c86b8b Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Mon, 19 Jan 2026 14:30:36 +0300
Subject: [PATCH] Fix deadlock detector activation in startup process
---
src/backend/utils/misc/timeout.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/backend/utils/misc/timeout.c b/src/backend/utils/misc/timeout.c
index ddba5dc607c..3ef819949de 100644
--- a/src/backend/utils/misc/timeout.c
+++ b/src/backend/utils/misc/timeout.c
@@ -313,8 +313,10 @@ schedule_alarm(TimestampTz now)
* to trigger the interrupt is likely to be a bit later than
* signal_due_at. That's fine, for the same reasons described above.
*/
+ /*
if (signal_pending && nearest_timeout >= signal_due_at)
return;
+ */
/*
* As with calling enable_alarm(), we must set signal_pending *before*
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-01-23 11:51 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-01-23 11:51 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org
Dear Hackers,
I would like to propose a patch that fixes the problem, which has the roots in
the possibility of spontaneous SIGALRM signals when waiting for some timeouts.
The idea of the patch - ignore spontaneous SIGALRM signals and continue waiting
for expected timeouts or buffer unpinning by the conflicting backend. This
patch is not a final version. I plan to add a tap-test for this case.
I'm in doubt to put the calls of some page buffer specific functions into
ResolveRecoveryConflictWithBufferPin (standby.c), but otherwise we have to
do more changes in LockBufferForCleanup and ResolveRecoveryConflictWithBufferPin.
I also think, we have to add some description of the found problem in timeout.c,
because the implemented optimization of setitimer calls leads to some not
evident consequences. The optimization seems to be implemented in the commit:
09cf1d52267644cdbdb734294012cf1228745aaa
With best regards,
Vitaly
Attachments:
v1-0001-Fix-deadlock-detector-activation-in-a-recovery-confl.patch (text/x-patch, 6.5K) download
From f31ae975dfba0271d8dd61b44008fc2de14df68b Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Fri, 23 Jan 2026 14:20:05 +0300
Subject: [PATCH] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 2 +-
src/backend/storage/ipc/standby.c | 80 +++++++++++++++++++----------
src/include/storage/standby.h | 2 +-
3 files changed, 54 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6f935648ae9..3b9fb78842a 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6627,7 +6627,7 @@ LockBufferForCleanup(Buffer buffer)
/* Publish the bufid that Startup process waits on */
SetStartupBufferPinWaitBufId(buffer - 1);
/* Set alarm and then wait to be signaled by UnpinBuffer() */
- ResolveRecoveryConflictWithBufferPin();
+ ResolveRecoveryConflictWithBufferPin(buffer);
/* Reset the published bufid */
SetStartupBufferPinWaitBufId(-1);
}
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index afffab77106..fde5f45781f 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -30,6 +30,7 @@
#include "storage/procarray.h"
#include "storage/sinvaladt.h"
#include "storage/standby.h"
+#include "storage/buf_internals.h"
#include "utils/hsearch.h"
#include "utils/injection_point.h"
#include "utils/ps_status.h"
@@ -790,11 +791,13 @@ cleanup:
* at least deadlock_timeout.
*/
void
-ResolveRecoveryConflictWithBufferPin(void)
+ResolveRecoveryConflictWithBufferPin(Buffer buffer)
{
TimestampTz ltime;
Assert(InHotStandby);
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
ltime = GetStandbyLimitTime();
@@ -831,35 +834,56 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
- *
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
- */
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
+ * by one of the timeouts established above.
+ *
+ * We assume that only UnpinBuffer() and the timeout requests established
+ * above can wake us up here. WakeupRecovery() called by walreceiver or
+ * SIGHUP signal handler, etc cannot do that because it uses the different
+ * latch from that ProcWaitForSignal() waits on.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ if (got_standby_delay_timeout)
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves for
+ * deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
+ * to be signaled by UnpinBuffer() again and send a request for
+ * deadlocks check if deadlock_timeout happens. This causes the
+ * request to continue to be sent every deadlock_timeout until the
+ * buffer is unpinned or ltime is reached. This would increase the
+ * workload in the startup process and backends. In practice it may
+ * not be so harmful because the period that the buffer is kept pinned
+ * is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ }
+ else
+ {
+ BufferDesc *bufHdr = GetBufferDescriptor(buffer - 1);
+ uint64 buf_state = LockBufHdr(bufHdr);
+ uint32 buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ UnlockBufHdr(bufHdr);
+
+ // When the buffer’s reference count exceeds 1, the exclusive lock
+ // remains unacquired. A SIGALRM signal appears to have been received
+ // unexpectedly, and it is not associated with any active timeout.
+ // The system should wait until either the buffer becomes unlocked
+ // or the anticipated timeout period elapses.
+ if (buf_refcount > 1)
+ continue;
+ }
+
+ break;
}
/*
diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h
index 7b10932635a..e15dcd22e8f 100644
--- a/src/include/storage/standby.h
+++ b/src/include/storage/standby.h
@@ -38,7 +38,7 @@ extern void ResolveRecoveryConflictWithTablespace(Oid tsid);
extern void ResolveRecoveryConflictWithDatabase(Oid dbid);
extern void ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict);
-extern void ResolveRecoveryConflictWithBufferPin(void);
+extern void ResolveRecoveryConflictWithBufferPin(Buffer buffer);
extern void CheckRecoveryConflictDeadlock(void);
extern void StandbyDeadLockHandler(void);
extern void StandbyTimeoutHandler(void);
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-05-20 16:26 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-05-20 16:26 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Fri, Jan 23, 2026 at 8:52 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Dear Hackers,
>
> I would like to propose a patch that fixes the problem, which has the roots in
> the possibility of spontaneous SIGALRM signals when waiting for some timeouts.
> The idea of the patch - ignore spontaneous SIGALRM signals and continue waiting
> for expected timeouts or buffer unpinning by the conflicting backend. This
> patch is not a final version. I plan to add a tap-test for this case.
Thanks for the patch!
#include "storage/sinvaladt.h"
#include "storage/standby.h"
+#include "storage/buf_internals.h"
This include should be placed in alphabetical order.
+ * We assume that only UnpinBuffer() and the timeout requests established
+ * above can wake us up here. WakeupRecovery() called by walreceiver or
+ * SIGHUP signal handler, etc cannot do that because it uses the different
+ * latch from that ProcWaitForSignal() waits on.
As your investigation showed, that assumption does not seem to hold. If so,
I think something like the following would be more accurate:
---------------------------------
ProcWaitForSignal() can also wake up for unrelated reasons, so recheck
whether we're still the waiter after each wakeup. If we are and no timeout
fired, continue waiting without resetting the active timeouts.
---------------------------------
+ uint32 buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
<snip>
+ if (buf_refcount > 1)
+ continue;
Wouldn't it be better to check explicitly whether we're still the waiter,
instead of using BUF_STATE_GET_REFCOUNT()?
(buf_state & BM_PIN_COUNT_WAITER) != 0 &&
bufHdr->wait_backend_pgprocno == MyProcNumber
The current control flow in the loop feels a bit hard to follow.
Would something like the following be simpler?
for (;;)
{
....
ProcWaitForSignal(...);
if (!StillWaitingForBufferPin(...))
break;
if (got_standby_delay_timeout)
{
SendRecoveryConflictWithBufferPin(...);
break;
}
else if (got_standby_deadlock_timeout)
{
SendRecoveryConflictWithBufferPin(...);
break;
}
}
Regards,
--
Fujii Masao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-05-25 14:20 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-05-25 14:20 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Dear Fujii Masao,
Thank you for the review of the patch. Based on your comments I propose
a new version of the patch.
> +#include "storage/buf_internals.h"
> This include should be placed in alphabetical order.
I removed the include of the header file. I'm not sure, that it is a good
way to expose buffer internals here. Instead, I implemented a new function
BufferGetRefCount to be used in standby.c.
> + uint32 buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
> <snip>
> + if (buf_refcount > 1)
> + continue;
>
> Wouldn't it be better to check explicitly whether we're still the waiter,
> instead of using BUF_STATE_GET_REFCOUNT()?
>
> (buf_state & BM_PIN_COUNT_WAITER) != 0 &&
> bufHdr->wait_backend_pgprocno == MyProcNumber
This is a precondition for the ResolveRecoveryConflictWithBufferPin function
that the current process should be a waiter process. See LockBufferForCleanup
where this state is set before the call of ResolveRecoveryConflictWithBufferPin.
I believe, there is no sense to check the waiter state because it doesn't
change in this function. Instead, I think, it is enough to just check for
refcount. One of the suggestions is to add an assert to check that the current
process is a waiter process, but I'm not sure about it. Let me know please if
I missed anything.
> The current control flow in the loop feels a bit hard to follow.
> Would something like the following be simpler?
I reorganized the control flow as well.
I also added a new tap-test as a part of the patch. I did some changes in the
tap test to make it stable. Let me know, please, if it should be in a separate
commit.
With best regards,
Vitaly
Attachments:
v2-0001-Fix-deadlock-detector-activation-in-a-recovery-confl.patch (text/x-patch, 11.8K) download
From fc0b3b6df48d7c5d69edf891cd84f8c4848a0e65 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 20 May 2026 11:48:34 +0300
Subject: [PATCH] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 25 +++-
src/backend/storage/ipc/standby.c | 67 +++++----
src/include/storage/bufmgr.h | 1 +
src/include/storage/standby.h | 2 +-
.../t/053_startup_backend_deadlock.pl | 127 ++++++++++++++++++
5 files changed, 194 insertions(+), 28 deletions(-)
create mode 100644 src/test/recovery/t/053_startup_backend_deadlock.pl
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc398db124d..ce5871fbd9b 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4749,6 +4749,29 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferGetRefCount
+ * Return the current reference counter.
+ */
+uint32
+BufferGetRefCount(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ UnlockBufHdr(bufHdr);
+
+ return buf_refcount;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -6789,7 +6812,7 @@ LockBufferForCleanup(Buffer buffer)
/* Publish the bufid that Startup process waits on */
SetStartupBufferPinWaitBufId(buffer - 1);
/* Set alarm and then wait to be signaled by UnpinBuffer() */
- ResolveRecoveryConflictWithBufferPin();
+ ResolveRecoveryConflictWithBufferPin(buffer);
/* Reset the published bufid */
SetStartupBufferPinWaitBufId(-1);
}
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..00399eafee3 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,13 +790,17 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The precondition: the current process should be the waiter process.
*/
void
-ResolveRecoveryConflictWithBufferPin(void)
+ResolveRecoveryConflictWithBufferPin(Buffer buffer)
{
TimestampTz ltime;
Assert(InHotStandby);
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
ltime = GetStandbyLimitTime();
@@ -833,35 +837,46 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
+ * by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so recheck
+ * that the buffer is pinned by the current waiter process only (reference
+ * counter should be 1). Continue waiting, if no registered timeout is
+ * fired or the buffer is still pinned by other processes as well.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is less or equal to 1 and we are the waiter
+ * process, no one uses the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
+ */
+ if (BufferGetRefCount(buffer) <= 1)
+ break;
+
+ if (got_standby_delay_timeout)
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves for
+ * deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
+ * to be signaled by UnpinBuffer() again and send a request for
+ * deadlocks check if deadlock_timeout happens. This causes the
+ * request to continue to be sent every deadlock_timeout until the
+ * buffer is unpinned or ltime is reached. This would increase the
+ * workload in the startup process and backends. In practice it may
+ * not be so harmful because the period that the buffer is kept pinned
+ * is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..b7dcff85a8d 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -313,6 +313,7 @@ extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
+uint32 BufferGetRefCount(Buffer buffer);
extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std);
diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h
index 6a314c693cd..c75d87b7ddc 100644
--- a/src/include/storage/standby.h
+++ b/src/include/storage/standby.h
@@ -79,7 +79,7 @@ extern void ResolveRecoveryConflictWithTablespace(Oid tsid);
extern void ResolveRecoveryConflictWithDatabase(Oid dbid);
extern void ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict);
-extern void ResolveRecoveryConflictWithBufferPin(void);
+extern void ResolveRecoveryConflictWithBufferPin(Buffer buffer);
extern void CheckRecoveryConflictDeadlock(void);
extern void StandbyDeadLockHandler(void);
extern void StandbyTimeoutHandler(void);
diff --git a/src/test/recovery/t/053_startup_backend_deadlock.pl b/src/test/recovery/t/053_startup_backend_deadlock.pl
new file mode 100644
index 00000000000..5bd24f315e6
--- /dev/null
+++ b/src/test/recovery/t/053_startup_backend_deadlock.pl
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+#
+# Test a deadlock between a backend and the startup processes when processing
+# XLOG_PRUNE_PAGE wal record. The test is based on 031_recovery_conflict.pl
+# vanilla test.
+#
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test settings
+my $deadlock_timeout = 3000; # ms
+my $log_startup_progress_interval = 2000; # ms
+my $testdb = "testdb";
+my $backup_name = 'my_backup';
+my $table1 = "table1";
+my $table2 = "table2";
+
+# Set up nodes
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1);
+
+$node_primary->append_conf('postgresql.conf', qq[
+
+ # for deadlock test
+ max_prepared_transactions = 10
+
+ # wait some to test the wait paths as well, but not long for obvious reasons
+ max_standby_streaming_delay = -1
+
+ # Some of the recovery conflict logging code only gets exercised after
+ # deadlock_timeout. The test doesn't rely on that additional output, but it's
+ # nice to get some minimal coverage of that code.
+ log_recovery_conflict_waits = on
+
+ log_startup_progress_interval = ${log_startup_progress_interval}
+ deadlock_timeout = ${deadlock_timeout}
+ autovacuum = off
+]);
+
+$node_primary->start;
+$node_primary->backup($backup_name);
+
+my $node_standby = PostgreSQL::Test::Cluster->new('standby');
+$node_standby->init_from_backup($node_primary, $backup_name, has_streaming => 1);
+$node_standby->start();
+
+my $log_location = -s $node_standby->logfile;
+
+# use a new database, to trigger database recovery conflict
+$node_primary->safe_psql('postgres', "CREATE DATABASE $testdb");
+#$node_primary->safe_psql('postgres', "CREATE TABLE $table2(a int, b int)");
+
+$node_primary->safe_psql($testdb, qq[
+ CREATE TABLE $table1(a int, b int);
+ CREATE TABLE $table2(a int, b int);
+ INSERT INTO $table1 VALUES (1);
+]);
+
+# Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
+# lock on another relation in a prepared xact, so it's held continuously by
+# the startup process. The standby psql will block acquiring that lock while
+# holding a pin that vacuum needs, triggering the deadlock.
+$node_primary->safe_psql($testdb, qq[
+ BEGIN;
+ INSERT INTO $table1(a) SELECT generate_series(1, 100) i;
+ ROLLBACK;
+]);
+
+$node_primary->safe_psql($testdb, qq[
+ BEGIN;
+ LOCK TABLE $table2;
+ PREPARE TRANSACTION 'lock';
+ INSERT INTO $table1(a) VALUES (170);
+ SELECT txid_current();
+]);
+
+$node_primary->wait_for_catchup($node_standby, 'replay', $node_primary->lsn('write'));
+
+my $psql_standby = $node_standby->background_psql($testdb, on_error_stop => 0);
+
+$psql_standby->query_until(qr/^1$/m, qq[
+ BEGIN;
+ -- hold pin
+ DECLARE test_recovery_conflict_cursor CURSOR FOR SELECT a FROM $table1;
+ FETCH FORWARD FROM test_recovery_conflict_cursor;
+ -- wait for lock held by prepared transaction
+ SELECT * FROM $table2;
+]);
+
+ok(1, "cursor holding conflicting pin, also waiting for lock, established");
+
+# VACUUM will prune away rows, causing a buffer pin conflict, while standby
+# psql is waiting on lock
+$node_primary->safe_psql($testdb, qq[VACUUM $table1;]);
+
+# Wait and check that the deadlock detector was triggered and found a deadlock
+# in the backend process (not in startup process).
+note("Waiting for deadlock detector to launch...");
+check_conflict_log("User transaction caused buffer deadlock with recovery.");
+
+# clean up for next tests
+$node_primary->safe_psql($testdb, qq[ROLLBACK PREPARED 'lock';]);
+
+# Check that expected number of conflicts show in pg_stat_database. Needs to
+# be tested before database is dropped, for obvious reasons.
+my $nconflicts = $node_standby->safe_psql($testdb, "SELECT conflicts FROM pg_stat_database WHERE datname = '$testdb'");
+note("number of recovery conflicts: $nconflicts");
+
+$psql_standby->quit();
+
+sub check_conflict_log
+{
+ my $message = shift;
+ my $old_log_location = $log_location;
+
+ $log_location = $node_standby->wait_for_log(qr/$message/, $log_location);
+
+ cmp_ok($log_location, '>', $old_log_location,
+ "logfile contains terminated connection due to recovery conflict"
+ );
+}
+
+done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-05-26 15:01 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-05-26 15:01 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Mon, May 25, 2026 at 11:20 PM Vitaly Davydov
<v.davydov@postgrespro.ru> wrote:
>
> Dear Fujii Masao,
>
> Thank you for the review of the patch. Based on your comments I propose
> a new version of the patch.
Thanks for updating the patch!
+ if (got_standby_delay_timeout)
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ else if (got_standby_deadlock_timeout)
+ {
Shouldn't we break out of the loop when either got_standby_delay_timeout or
got_standby_deadlock_timeout becomes true? Otherwise, the loop continues with
those flags still set, which could cause SendRecoveryConflictWithBufferPin() to
be called unnecessarily in the subsequent cycles.
+ if (BufferGetRefCount(buffer) <= 1)
Should this be "BufferGetRefCount(buffer) == 1" instead? I don't think
BufferGetRefCount(buffer) should ever return 0 here. If that's correct,
would it make sense to explicitly detect that case, for example:
-----------------
uint32 refcount = BufferGetRefCount(buffer);
Assert(refcount > 0);
if (refcount == 0)
elog(ERROR, "buffer refcount dropped to zero while waiting for
cleanup lock");
if (refcount == 1)
break;
-----------------
> I also added a new tap-test as a part of the patch. I did some changes in the
> tap test to make it stable. Let me know, please, if it should be in a separate
> commit.
Do we really need a new TAP test file for this? We already have
a startup/backend deadlock test in t/031_recovery_conflict.pl. Extending
the deadlock section there to cover this test case seems simpler and easier to
follow than introducing a new t/053_* test.
Regards,
--
Fujii Masao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-05 11:58 Fujii Masao <masao.fujii@gmail.com>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 2 replies; 43+ messages in thread
From: Fujii Masao @ 2026-06-05 11:58 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Wed, May 27, 2026 at 12:01 AM Fujii Masao <masao.fujii@gmail.com> wrote:
> + if (got_standby_delay_timeout)
> + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
> + else if (got_standby_deadlock_timeout)
> + {
>
> Shouldn't we break out of the loop when either got_standby_delay_timeout or
> got_standby_deadlock_timeout becomes true? Otherwise, the loop continues with
> those flags still set, which could cause SendRecoveryConflictWithBufferPin() to
> be called unnecessarily in the subsequent cycles.
>
>
> + if (BufferGetRefCount(buffer) <= 1)
>
> Should this be "BufferGetRefCount(buffer) == 1" instead? I don't think
> BufferGetRefCount(buffer) should ever return 0 here. If that's correct,
> would it make sense to explicitly detect that case, for example:
>
> -----------------
> uint32 refcount = BufferGetRefCount(buffer);
>
> Assert(refcount > 0);
>
> if (refcount == 0)
> elog(ERROR, "buffer refcount dropped to zero while waiting for
> cleanup lock");
>
> if (refcount == 1)
> break;
> -----------------
I've updated the patch based on these comments.
Attached is the latest version.
I removed the TAP test from this patch for now. I'll consider adding
a test for this separately later.
BTW, I'm just wondering whether ResolveRecoveryConflictWithLock()
might have the same issue. I need to investigate that further.
Regards,
--
Fujii Masao
Attachments:
v3-0001-Fix-deadlock-detector-activation-in-a-recovery-co.patch (application/octet-stream, 6.5K) download
From 0941469babffc27ad1ae415db95a878d0a8eabdb Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 20 May 2026 11:48:34 +0300
Subject: [PATCH v3] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 23 ++++++++
src/backend/storage/ipc/standby.c | 81 ++++++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 80 insertions(+), 25 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc398db124d..7502b461786 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4749,6 +4749,29 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferGetRefCount
+ * Return the current reference counter for a shared buffer.
+ */
+uint32
+BufferGetRefCount(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ UnlockBufHdr(bufHdr);
+
+ return buf_refcount;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..8cea201df5f 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,14 +790,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -833,35 +840,59 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
+ uint32 refcount;
+
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck that the buffer is pinned by the current waiter process
+ * only (reference counter should be 1). Continue waiting, if no
+ * registered timeout is fired or the buffer is still pinned by other
+ * processes as well.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ refcount = BufferGetRefCount(bufid + 1);
+ Assert(refcount > 0);
+
+ if (refcount == 0)
+ elog(ERROR,
+ "buffer refcount dropped to zero while waiting for cleanup lock");
+ if (refcount == 1)
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..c38b620a3a1 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -311,6 +311,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern uint32 BufferGetRefCount(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.53.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-09 16:18 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-09 16:18 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
On 6/5/26 14:58, Fujii Masao wrote:
> I've updated the patch based on these comments.
> Attached is the latest version.
>
> I removed the TAP test from this patch for now. I'll consider adding
> a test for this separately later.
Thank you again for your assistance. I agree with the code changes you've
proposed. I've updated 031_recovery_conflict.pl to include the deadlock
test, instead of creating a separate TAP test. The attached patch contains
these changes in its second commit. I've kept the patch version as v3 since
the original fix remains unchanged; this update only adds new tests.
With best regards,
Vitaly
Attachments: download all (2 files, .tar.gz)
v3-0001-Fix-deadlock-detector-activation-in-a-recovery-confl.patch (text/x-patch, 6.5K) download
From a2287174d5c595752f7e5c447807af219c78582e Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 20 May 2026 11:48:34 +0300
Subject: [PATCH 1/2] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 23 ++++++++
src/backend/storage/ipc/standby.c | 81 ++++++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 80 insertions(+), 25 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc398db124d..7502b461786 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4749,6 +4749,29 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferGetRefCount
+ * Return the current reference counter for a shared buffer.
+ */
+uint32
+BufferGetRefCount(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ UnlockBufHdr(bufHdr);
+
+ return buf_refcount;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..8cea201df5f 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,14 +790,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -833,35 +840,59 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
+ uint32 refcount;
+
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck that the buffer is pinned by the current waiter process
+ * only (reference counter should be 1). Continue waiting, if no
+ * registered timeout is fired or the buffer is still pinned by other
+ * processes as well.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ refcount = BufferGetRefCount(bufid + 1);
+ Assert(refcount > 0);
+
+ if (refcount == 0)
+ elog(ERROR,
+ "buffer refcount dropped to zero while waiting for cleanup lock");
+ if (refcount == 1)
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..c38b620a3a1 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -311,6 +311,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern uint32 BufferGetRefCount(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v3-0002-Add-new-deadlock-conflict-test-in-031_recovery_confl.patch (text/x-patch, 4.7K) download
From dfdb8e0e37ecbce515bbff52bf742e47e0d063a6 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Tue, 9 Jun 2026 18:23:28 +0300
Subject: [PATCH 2/2] Add new deadlock conflict test in
031_recovery_conflict.pl
It checks that the deadlock detector is triggered on the hot standby's
client backend process when a deadlock with the startup process occurs
in the scenario of an interference of log_startup_process_interval and
deadlock_timeout timeouts.
An infinite deadlock occurs between the standby's startup process and a
backend process when handling an XLOG_PRUNE_PAGE record. The issue arises
because the startup process fails to trigger the deadlock detector in the
conflicting backend.
---
src/test/recovery/t/031_recovery_conflict.pl | 100 +++++++++++++++++++
1 file changed, 100 insertions(+)
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index 7a740f69806..cac78443d0c 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -307,6 +307,106 @@ $psql_standby->quit;
$node_standby->stop();
$node_primary->stop();
+## RECOVERY CONFLICT 6: Startup and backend infinite deadlock on hot standby
+#
+# An infinite deadlock occurs between the standby's startup process and a
+# backend process when handling an XLOG_PRUNE_PAGE record. The issue arises
+# because the startup process fails to trigger the deadlock detector in the
+# conflicting backend.
+#
+# The root cause is interference between two timeouts: deadlock_timeout and
+# log_startup_progress_interval. Due to this interference, the
+# deadlock_timeout event never fires for the startup process.
+#
+# This behavior is a side effect of the timeout optimization logic implemented
+# in timeout.c. While this optimization improves performance by coalescing
+# timer events, it introduces a requirement that all timeout handlers must
+# verify if their specific timeout was indeed triggered before proceeding. In
+# this case, the handler responsible for invoking the deadlock detector does
+# not correctly account for this interference, leading to the failure.
+#
+# Discussion: https://www.postgresql.org/message-id/flat/44c24dcf-5710-410f-b1b6-d10b315f3d51%40postgrespro.ru
+#
+
+$test_db = 'postgres';
+$table1 = "test_startup_backend_deadlock_table_1";
+$table2 = "test_startup_backend_deadlock_table_2";
+
+$node_primary->append_conf('postgresql.conf', qq[
+ max_standby_streaming_delay = -1
+ log_startup_progress_interval = 2000
+ deadlock_timeout = 3000
+ autovacuum = off
+]);
+
+$node_primary->start();
+
+$node_standby->append_conf('postgresql.conf', qq[
+ max_standby_streaming_delay = -1
+ log_startup_progress_interval = 2000
+ deadlock_timeout = 3000
+ autovacuum = off
+]);
+
+$node_standby->start();
+
+$log_location = -s $node_standby->logfile;
+
+$node_primary->safe_psql('postgres', qq[
+ CREATE TABLE $table1(a int, b int);
+ CREATE TABLE $table2(a int, b int);
+ INSERT INTO $table1 VALUES (1);
+]);
+
+# Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
+# lock on another relation in a prepared xact, so it's held continuously by
+# the startup process. The standby psql will block acquiring that lock while
+# holding a pin that vacuum needs, triggering the deadlock.
+$node_primary->safe_psql($test_db, qq[
+ BEGIN;
+ INSERT INTO $table1(a) SELECT generate_series(1, 100) i;
+ ROLLBACK;
+]);
+
+$node_primary->safe_psql($test_db, qq[
+ BEGIN;
+ LOCK TABLE $table2;
+ PREPARE TRANSACTION 'lock';
+ INSERT INTO $table1(a) VALUES (170);
+ SELECT txid_current();
+]);
+
+$node_primary->wait_for_catchup($node_standby, 'replay', $node_primary->lsn('write'));
+
+$psql_standby = $node_standby->background_psql($test_db, on_error_stop => 0);
+
+$psql_standby->query_until(qr/^1$/m, qq[
+ BEGIN;
+ -- hold pin
+ DECLARE test_recovery_conflict_cursor CURSOR FOR SELECT a FROM $table1;
+ FETCH FORWARD FROM test_recovery_conflict_cursor;
+ -- wait for lock held by prepared transaction
+ SELECT * FROM $table2;
+]);
+
+ok(1, "cursor holding conflicting pin, also waiting for lock, established");
+
+# VACUUM will prune away rows, causing a buffer pin conflict, while standby
+# psql is waiting on lock.
+$node_primary->safe_psql($test_db, qq[VACUUM $table1;]);
+
+# Wait and check that the deadlock detector was triggered and found a deadlock
+# in the backend process (not in startup process).
+note("Waiting for deadlock detector to launch...");
+check_conflict_log("User transaction caused buffer deadlock with recovery.");
+
+# clean up for next tests
+$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
+
+$psql_standby->quit();
+
+$node_standby->stop();
+$node_primary->stop();
done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-10 08:42 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-10 08:42 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Wed, Jun 10, 2026 at 1:18 AM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> On 6/5/26 14:58, Fujii Masao wrote:
> > I've updated the patch based on these comments.
> > Attached is the latest version.
> >
> > I removed the TAP test from this patch for now. I'll consider adding
> > a test for this separately later.
>
> Thank you again for your assistance. I agree with the code changes you've
> proposed. I've updated 031_recovery_conflict.pl to include the deadlock
> test, instead of creating a separate TAP test. The attached patch contains
> these changes in its second commit. I've kept the patch version as v3 since
> the original fix remains unchanged; this update only adds new tests.
Thanks for updating the regression test patch!
I reviewed both patches and found a few issues, so I fixed them and
attached updated v4 versions.
For v3-0001, when the startup process wakes up from
ProcWaitForSignal() and finds the pin count is still not 1, it waits
again. However, before waiting again, it must set
BM_PIN_COUNT_WAITER again. Otherwise, a backend releasing the pin
cannot signal the startup process because the flag was already cleared.
For example:
1. Backend A releases the pin, signals the startup process, and clears
BM_PIN_COUNT_WAITER.
2. The startup process wakes up from ProcWaitForSignal().
3. Before it rechecks the pin count, backend B acquires a pin.
4. The startup process sees the pin count is still 2.
5. The startup process waits again.
At step 5, BM_PIN_COUNT_WAITER is already cleared, so backend B will
not signal the startup process when releasing the pin.
This race is unlikely in practice, but it is still possible. So in
v4-0001, I fixed this by re-registering the startup process as the
waiter before waiting again.
For v3-0002, the two deadlock tests contained very similar scenarios.
I think it's simpler to move the common logic into a shared subroutine.
So in v4-0002, I added a helper routine for testing the deadlock
between the startup process and a backend, and both tests now use it.
Could you review the v4 patches?
Regards,
--
Fujii Masao
Attachments: download all (2 files, .tar.gz)
v4-0002-Add-new-deadlock-conflict-test-in-031_recovery_co.patch (application/octet-stream, 7.8K) download
From 11f20b33db27013cfd39ac878c0e453a302eca2c Mon Sep 17 00:00:00 2001
From: "masao.fujii" <masao.fujii@masao.fujii’s-MacBook-Pro>
Date: Wed, 10 Jun 2026 12:12:55 +0900
Subject: [PATCH v4 2/2] Add new deadlock conflict test in
031_recovery_conflict.pl
It checks that the deadlock detector is triggered on the hot standby's
client backend process when a deadlock with the startup process occurs
in the scenario of an interference of log_startup_process_interval and
deadlock_timeout timeouts.
An infinite deadlock occurs between the standby's startup process and a
backend process when handling an XLOG_PRUNE_PAGE record. The issue arises
because the startup process fails to trigger the deadlock detector in the
conflicting backend.
---
src/test/recovery/t/031_recovery_conflict.pl | 166 ++++++++++++-------
1 file changed, 109 insertions(+), 57 deletions(-)
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index 7a740f69806..06de8b51668 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -223,63 +223,7 @@ $node_standby->adjust_conf(
$node_standby->restart();
$psql_standby->reconnect_and_clear();
-# Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
-# lock on another relation in a prepared xact, so it's held continuously by
-# the startup process. The standby psql will block acquiring that lock while
-# holding a pin that vacuum needs, triggering the deadlock.
-$node_primary->safe_psql(
- $test_db,
- qq[
-CREATE TABLE $table1(a int, b int);
-INSERT INTO $table1 VALUES (1);
-BEGIN;
-INSERT INTO $table1(a) SELECT generate_series(1, 100) i;
-ROLLBACK;
-BEGIN;
-LOCK TABLE $table2;
-PREPARE TRANSACTION 'lock';
-INSERT INTO $table1(a) VALUES (170);
-SELECT txid_current();
-]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-$res = $psql_standby->query_until(
- qr/^1$/m, qq[
- BEGIN;
- -- hold pin
- DECLARE $cursor1 CURSOR FOR SELECT a FROM $table1;
- FETCH FORWARD FROM $cursor1;
- -- wait for lock held by prepared transaction
- SELECT * FROM $table2;
- ]);
-ok(1,
- "$sect: cursor holding conflicting pin, also waiting for lock, established"
-);
-
-# just to make sure we're waiting for lock already
-ok( $node_standby->poll_query_until(
- 'postgres', qq[
-SELECT 'waiting' FROM pg_locks WHERE locktype = 'relation' AND NOT granted;
-], 'waiting'),
- "$sect: lock acquisition is waiting");
-
-# VACUUM FREEZE will prune away rows, causing a buffer pin conflict, while
-# standby psql is waiting on lock
-$node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log("User transaction caused buffer deadlock with recovery.");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("deadlock");
-
-# clean up for next tests
-$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
-$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
- '50ms');
-$node_standby->restart();
-$psql_standby->reconnect_and_clear();
-
+test_deadlock_conflict_scenario();
# Check that expected number of conflicts show in pg_stat_database. Needs to
# be tested before database is dropped, for obvious reasons.
@@ -289,6 +233,11 @@ is( $node_standby->safe_psql(
$expected_conflicts,
qq[$expected_conflicts recovery conflicts shown in pg_stat_database]);
+$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
+ '50ms');
+$node_standby->restart();
+$psql_standby->reconnect_and_clear();
+
## RECOVERY CONFLICT 6: Database conflict
$sect = "database conflict";
@@ -299,6 +248,47 @@ $node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log("User was connected to a database that must be dropped");
+## FOLLOW-UP SCENARIO: startup deadlock from coalesced timeouts
+#
+# This is an additional deadlock-coverage scenario, not a new numbered
+# recovery-conflict type. It stays here, after the numbered conflict cases,
+# because the main test database has already been dropped above and this
+# follow-up can run independently in the always-present "postgres" database.
+#
+# The scenario exercises an infinite deadlock between the standby startup
+# process and a standby backend while replaying XLOG_PRUNE_PAGE. The backend
+# holds a buffer pin and then waits for a relation lock; replay of VACUUM
+# needs that pin to be released, but the lock is held on behalf of replay.
+#
+# The bug was caused by interference between deadlock_timeout and
+# log_startup_progress_interval: timeout.c can coalesce timer events, so each
+# timeout handler must verify that its own timeout really fired. In this case
+# the startup process could miss the deadlock_timeout event and fail to ask
+# the conflicting backend to run deadlock detection.
+
+$sect = "startup deadlock from coalesced timeouts";
+
+# Use the always-present postgres database so this follow-up remains
+# independent of the earlier database-conflict case that dropped $test_db.
+$test_db = 'postgres';
+
+$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
+ '-1');
+$node_standby->adjust_conf('postgresql.conf',
+ 'log_startup_progress_interval', '2s');
+$node_standby->adjust_conf('postgresql.conf', 'deadlock_timeout', '3s');
+$node_standby->restart();
+$psql_standby = $node_standby->background_psql($test_db, on_error_stop => 0);
+
+test_deadlock_conflict_scenario();
+
+# Check that the additional startup/backend deadlock scenario is also counted
+# in pg_stat_database for the postgres database.
+is( $node_standby->safe_psql(
+ $test_db,
+ qq[SELECT conflicts FROM pg_stat_database WHERE datname='$test_db';]),
+ 1,
+ qq[1 recovery conflict shown in pg_stat_database for $test_db]);
# explicitly shut down psql instances gracefully - to avoid hangs or worse on
# windows
@@ -331,3 +321,65 @@ sub check_conflict_stat
is($count, 1, "$sect: stats show conflict on standby");
}
+
+sub test_deadlock_conflict_scenario
+{
+ my $res;
+
+ # Generate a few dead rows, to later be cleaned up by vacuum. Then
+ # acquire a lock on another relation in a prepared xact, so it's held
+ # continuously by the startup process. The standby psql will block
+ # acquiring that lock while holding a pin that vacuum needs, triggering
+ # the deadlock.
+ $node_primary->safe_psql(
+ $test_db,
+ qq[
+CREATE TABLE IF NOT EXISTS $table1(a int, b int);
+CREATE TABLE IF NOT EXISTS $table2(a int, b int);
+INSERT INTO $table1 VALUES (1);
+BEGIN;
+INSERT INTO $table1(a) SELECT generate_series(1, 100) i;
+ROLLBACK;
+BEGIN;
+LOCK TABLE $table2;
+PREPARE TRANSACTION 'lock';
+INSERT INTO $table1(a) VALUES (170);
+SELECT txid_current();
+]);
+
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ $res = $psql_standby->query_until(
+ qr/^1$/m, qq[
+ BEGIN;
+ -- hold pin
+ DECLARE $cursor1 CURSOR FOR SELECT a FROM $table1;
+ FETCH FORWARD FROM $cursor1;
+ -- wait for lock held by prepared transaction
+ SELECT * FROM $table2;
+]);
+
+ like($res, qr/^1$/m,
+ "$sect: cursor holding conflicting pin, also waiting for lock, established"
+ );
+
+ # Just to make sure we're waiting for the relation lock already.
+ ok( $node_standby->poll_query_until(
+ 'postgres', qq[
+SELECT 'waiting' FROM pg_locks WHERE locktype = 'relation' AND NOT granted;
+], 'waiting'),
+ "$sect: lock acquisition is waiting");
+
+ # VACUUM FREEZE will prune away rows, causing a buffer pin conflict, while
+ # standby psql is waiting on lock.
+ $node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ check_conflict_log(
+ "User transaction caused buffer deadlock with recovery.");
+ $psql_standby->reconnect_and_clear();
+ check_conflict_stat("deadlock");
+
+ # clean up for next tests
+ $node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
+}
--
2.53.0
v4-0001-Fix-deadlock-detector-activation-in-a-recovery-co.patch (application/octet-stream, 8.5K) download
From 385cbfe08d0e780c350bad78c9d1e4f283294a21 Mon Sep 17 00:00:00 2001
From: "masao.fujii" <masao.fujii@masao.fujii’s-MacBook-Pro>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v4 1/2] Fix deadlock detector activation in a recovery
conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 75 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 71 +++++++++++++++++----------
src/include/storage/bufmgr.h | 1 +
3 files changed, 117 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc398db124d..55fe467315e 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3455,6 +3455,28 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ UnlockBufHdrExt(bufHdr, buf_state, BM_PIN_COUNT_WAITER, 0, 0);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4749,6 +4771,53 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferShouldWaitForCleanupLockForStandby
+ * Recheck whether the startup process still needs to wait.
+ *
+ * This is only for the hot-standby path in ResolveRecoveryConflictWithBufferPin()
+ * after ProcWaitForSignal() returns.
+ *
+ * Returns true if other backends still pin the shared buffer. In that case,
+ * this function guarantees that the current backend remains registered as the
+ * pincount waiter to be woken when the buffer refcount drops to 1. Returns
+ * false when the caller itself is the only remaining pin holder, so the
+ * caller can retry taking the cleanup lock.
+ */
+bool
+BufferShouldWaitForCleanupLockForStandby(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return false;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait agan.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return true;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -6741,11 +6810,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- UnlockBufHdrExt(bufHdr, buf_state,
- BM_PIN_COUNT_WAITER, 0,
- 0);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..17e80f0d624 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,14 +790,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -833,35 +840,49 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later that the buffer is pinned by the current waiter
+ * process only (reference counter should be 1).
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ if (!BufferShouldWaitForCleanupLockForStandby(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..999862825ae 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -311,6 +311,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern bool BufferShouldWaitForCleanupLockForStandby(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.53.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-12 06:00 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-12 06:00 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Dear Fujii Masao,
On 6/10/26 11:42, Fujii Masao wrote:
> Could you review the v4 patches?
I've got the issue with BM_PIN_COUNT_WAITER. The origin of the issue is that
the other backend resets the flag before sending the notification (signal)
to the waiter. I agree with your changes where this flag is set again.
I would like to propose BufferIsReadyForCleanup function name instead of
BufferShouldWaitForCleanupLockForStandby (and change the returning value
semantics). There are other possible alternatives: BufferIsReadyForWriting,
BufferIsReadyForExclusiveAccess or something else. The code will look like
below:
if (BufferIsReadyForCleanup(bufid + 1))
break;
I've also found strange email in the commit messages like below:
Author: masao.fujii <masao.fujii@masao.fujii’s-MacBook-Pro>
I tried the modified 031 tap test but I do not see that it reproduces the issue
on the version without the fix. My version of 031 tap test hangs without the
fix. I haven't yet reviewed the tap test code at the moment, but I'm going to do
it as well.
Below one more note that is not directly related to the fix but may probably
improve the code readability.
1. BufferIsReadyForCleanup can be re-used with some small modifications in
LockBufferForCleanup like shown below:
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
if (BufferIsReadyForCleanup())
{
// log recovery conflict
return;
}
In this case, we have to reset PM_PIN_COUNT_WAITER state in BufferIsReadyForCleanup:
if (buf_refcount == 1)
{
bufHdr->wait_backend_procno = -1;
PinCountWaitBuf = NULL;
UnlockBufHdrExt(bufHdr, 0, PM_PIN_COUNT_WAITER);
return false;
}
I may to prepare one more commit with this change.
With best regards,
Vitaly
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-12 09:23 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-12 09:23 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Fri, Jun 12, 2026 at 3:00 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Dear Fujii Masao,
>
> On 6/10/26 11:42, Fujii Masao wrote:
> > Could you review the v4 patches?
>
> I've got the issue with BM_PIN_COUNT_WAITER. The origin of the issue is that
> the other backend resets the flag before sending the notification (signal)
> to the waiter. I agree with your changes where this flag is set again.
>
> I would like to propose BufferIsReadyForCleanup function name instead of
> BufferShouldWaitForCleanupLockForStandby (and change the returning value
> semantics). There are other possible alternatives: BufferIsReadyForWriting,
> BufferIsReadyForExclusiveAccess or something else. The code will look like
> below:
>
> if (BufferIsReadyForCleanup(bufid + 1))
> break;
Okay.
> I've also found strange email in the commit messages like below:
> Author: masao.fujii <masao.fujii@masao.fujii’s-MacBook-Pro>
That's my mistake. Sorry. I'll fix it in the next version of the patch.
> I tried the modified 031 tap test but I do not see that it reproduces the issue
> on the version without the fix. My version of 031 tap test hangs without the
> fix. I haven't yet reviewed the tap test code at the moment, but I'm going to do
> it as well.
I retried the modified 031 TAP test on my side without the fix, and
the test actually passed, so the test did not reliably reproduce the issue.
However, after increasing deadlock_timeout to 10s, the test hung
without the fix as expected, while it passed with the fix.
Based on this, I suspect the behavior depends on timing interactions
between deadlock_timeout and SIGALRM delivery caused by
log_startup_progress_interval. My test changes may simply have altered
the timing of those SIGALRM deliveries.
Also, the current test assumes that
log_startup_progress_interval is effective during standby WAL replay.
But as discussed in [1], that assumption may actually be wrong. It
seems to work today, but that could just be accidental and might
change in the future. So I think we should consider a testing approach
that does not depend on that assumption.
At the moment, the only alternative I can think of is to send SIGALRM
periodically to the startup process directly from the test, though I'm
not sure that's a good approach either...
> Below one more note that is not directly related to the fix but may probably
> improve the code readability.
I agree that some refactoring along those lines should be possible.
However, for backpatching to older branches, I'd prefer to avoid more
code churn than necessary for the bug fix itself. So I think it would
be better to commit the bug fix first, and then consider a refactoring
version separately for v20dev.
Regards,
[1] https://www.postgresql.org/message-id/flat/CAHGQGwFJixYTymPZ6dwOO-G0VdjO1AgZTma6CtJgc0Ptxfz3BA%40mai...
--
Fujii Masao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-18 02:48 Fujii Masao <masao.fujii@gmail.com>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-18 02:48 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Fri, Jun 12, 2026 at 6:23 PM Fujii Masao <masao.fujii@gmail.com> wrote:
>
> On Fri, Jun 12, 2026 at 3:00 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
> >
> > Dear Fujii Masao,
> >
> > On 6/10/26 11:42, Fujii Masao wrote:
> > > Could you review the v4 patches?
> >
> > I've got the issue with BM_PIN_COUNT_WAITER. The origin of the issue is that
> > the other backend resets the flag before sending the notification (signal)
> > to the waiter. I agree with your changes where this flag is set again.
> >
> > I would like to propose BufferIsReadyForCleanup function name instead of
> > BufferShouldWaitForCleanupLockForStandby (and change the returning value
> > semantics). There are other possible alternatives: BufferIsReadyForWriting,
> > BufferIsReadyForExclusiveAccess or something else. The code will look like
> > below:
> >
> > if (BufferIsReadyForCleanup(bufid + 1))
> > break;
>
> Okay.
I've updated the patch accordingly.
> Also, the current test assumes that
> log_startup_progress_interval is effective during standby WAL replay.
> But as discussed in [1], that assumption may actually be wrong. It
> seems to work today, but that could just be accidental and might
> change in the future. So I think we should consider a testing approach
> that does not depend on that assumption.
>
> At the moment, the only alternative I can think of is to send SIGALRM
> periodically to the startup process directly from the test, though I'm
> not sure that's a good approach either...
I still haven't come up with a good way to test this without depending on
log_startup_progress_interval being active during standby WAL replay.
Even so, I think the fix is still worth committing because it addresses
a real issue. So how about committing 0001 patch first, even without
a regression test?
Regards,
--
Fujii Masao
Attachments:
v5-0001-Fix-deadlock-detector-activation-in-a-recovery-co.patch (application/octet-stream, 8.5K) download
From 481aa2bd5b5ada647ffa57575c7b8436c3d17c40 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v5] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 77 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++----------
src/include/storage/bufmgr.h | 1 +
3 files changed, 118 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d6c0cc1f6d4..2c3260e904a 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3455,6 +3455,28 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ UnlockBufHdrExt(bufHdr, buf_state, BM_PIN_COUNT_WAITER, 0, 0);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4749,6 +4771,55 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -6741,11 +6812,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- UnlockBufHdrExt(bufHdr, buf_state,
- BM_PIN_COUNT_WAITER, 0,
- 0);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..f802870392a 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,14 +790,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -833,35 +840,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..10519342194 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -311,6 +311,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.53.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-18 13:11 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-18 13:11 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Dear Fujii Masao,
Thank you for the v5 patch. It looks good for me.
> I still haven't come up with a good way to test this without depending on
> log_startup_progress_interval being active during standby WAL replay.
> Even so, I think the fix is still worth committing because it addresses
> a real issue. So how about committing 0001 patch first, even without
> a regression test?
I have no objections to commit the v5 patch without the tap-test. Once, it is
the bug fix, should we prepare the fix for other major branches? I'm ready to
prepare the patches based on the v5 version.
I looked at the tap test and found the reason why the test doesn't reproduce
the issue. The function adjust_conf in Cluster.pm changes the existing
parameter but doesn't add new new parameter to the configuration file if it is
missed. It doesn't report any errors as well. If to use the append_conf method
the test starts to reproduce the issue pretty stable, because the parameter is
actually added to the configuration file.
One more note - in my original test I set autovacuum = off for all nodes, but
in modified 031 test autovacuum seems to be enabled. Disabling autovacuum may
make the test more stable.
With best regards,
Vitaly
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-18 16:22 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-18 16:22 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Thu, Jun 18, 2026 at 10:11 PM Vitaly Davydov
<v.davydov@postgrespro.ru> wrote:
>
> Dear Fujii Masao,
>
> Thank you for the v5 patch. It looks good for me.
Thanks for the review!
> I have no objections to commit the v5 patch without the tap-test. Once, it is
> the bug fix, should we prepare the fix for other major branches? I'm ready to
> prepare the patches based on the v5 version.
Yes, it's really helpful if you prepare the patches for stable versions.
> I looked at the tap test and found the reason why the test doesn't reproduce
> the issue. The function adjust_conf in Cluster.pm changes the existing
> parameter but doesn't add new new parameter to the configuration file if it is
> missed. It doesn't report any errors as well. If to use the append_conf method
> the test starts to reproduce the issue pretty stable, because the parameter is
> actually added to the configuration file.
>
> One more note - in my original test I set autovacuum = off for all nodes, but
> in modified 031 test autovacuum seems to be enabled. Disabling autovacuum may
> make the test more stable.
Thanks for the analysis!
Regards,
--
Fujii Masao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-22 18:32 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-22 18:32 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Dear Fujii Masao, All
While working on porting the patch to other majors, I've found that, the code
in RegisterPinCountWaiter() (see below) should be improved because it doesn't
unlock the bufHdr when the error is raised, as in LockBufferForCleanup().
I guess it works, but it keeps the locks a bit longer.
RegisterPinCountWaiter v5 patch code:
...
if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
bufHdr->wait_backend_pgprocno != MyProcNumber)
elog(ERROR, "multiple processes attempting to wait for pincount 1");
RegisterPinCountWaiter proposed changes:
...
if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
bufHdr->wait_backend_pgprocno != MyProcNumber)
{
UnlockBufHdr(bufHdr);
elog(ERROR, "multiple processes attempting to wait for pincount 1");
}
What do you think?
With best regards,
Vitaly
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-23 03:13 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-23 03:13 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Tue, Jun 23, 2026 at 3:32 AM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Dear Fujii Masao, All
>
> While working on porting the patch to other majors, I've found that, the code
> in RegisterPinCountWaiter() (see below) should be improved because it doesn't
> unlock the bufHdr when the error is raised, as in LockBufferForCleanup().
> I guess it works, but it keeps the locks a bit longer.
>
> RegisterPinCountWaiter v5 patch code:
> ...
> if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
> bufHdr->wait_backend_pgprocno != MyProcNumber)
> elog(ERROR, "multiple processes attempting to wait for pincount 1");
>
> RegisterPinCountWaiter proposed changes:
> ...
> if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
> bufHdr->wait_backend_pgprocno != MyProcNumber)
> {
> UnlockBufHdr(bufHdr);
> elog(ERROR, "multiple processes attempting to wait for pincount 1");
> }
>
> What do you think?
You're right. Thanks for pointing out that!
I've updated the patch accordingly.
Regards,
--
Fujii Masao
Attachments:
v6-0001-Fix-deadlock-detector-activation-in-a-recovery-co.patch (application/octet-stream, 8.5K) download
From 587ff41104e193718f4dd55b46da9d917a678a06 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6] Fix deadlock detector activation in a recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 121 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d6c0cc1f6d4..5512e14eadf 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3455,6 +3455,31 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ UnlockBufHdrExt(bufHdr, buf_state, BM_PIN_COUNT_WAITER, 0, 0);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4749,6 +4774,55 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -6741,11 +6815,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- UnlockBufHdrExt(bufHdr, buf_state,
- BM_PIN_COUNT_WAITER, 0,
- 0);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..b03943b7d46 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,14 +790,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -833,35 +840,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..10519342194 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -311,6 +311,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.53.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-24 15:16 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-24 15:16 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Dear Fujii Masao
I've prepared patches of v6 for other majors. I hope, it will help.
I will also think of a better approach for tap-test patch as well.
With best regards,
Vitaly
Attachments: download all (5 files, .tar.gz)
v6-REL_14_STABLE-0001-Fix-deadlock-detector-activation-in.patch (text/x-patch, 8.6K) download
From aee002a40ecffb5fcc31ec6392a3bcfc41cfa57b Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6-REL_14_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 81 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 71 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 123 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f563742ad44..a29b6f970bb 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1839,6 +1839,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pid == MyProcPid);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pid != MyProcPid)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pid = MyProcPid;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3119,6 +3145,56 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pid == MyProcPid);
+
+ if (buf_refcount == 1)
+ {
+ buf_state &= ~BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelFileNodeBuffers
*
@@ -4219,10 +4295,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pid = MyProcPid;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 49cccad9bf6..cb87f0b1fe1 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -749,14 +749,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -792,36 +799,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(
- PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 85caac3253d..3ef5dc7e349 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -223,6 +223,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void PrintPinnedBufs(void);
#endif
extern Size BufferShmemSize(void);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileNode *rnode,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6-REL_15_STABLE-0001-Fix-deadlock-detector-activation-in.patch (text/x-patch, 8.6K) download
From 4777798b041f6543b4e568d5c00d74d139b7f738 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6-REL_15_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 81 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 123 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index a02814a8f51..224cb677626 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1847,6 +1847,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProc->pgprocno)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3102,6 +3128,56 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if (buf_refcount == 1)
+ {
+ buf_state &= ~BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelFileNodeBuffers
*
@@ -4356,10 +4432,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index a36eb80e9ae..2b029cdcf10 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -750,14 +750,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -793,35 +800,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 8e93d315a67..d0a92541ed8 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -226,6 +226,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void PrintPinnedBufs(void);
#endif
extern Size BufferShmemSize(void);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileNode *rnode,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6-REL_16_STABLE-0001-Fix-deadlock-detector-activation-in.patch (text/x-patch, 8.6K) download
From 1a14d442072a1dc4a0063825fc7f366b755bb527 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6-REL_16_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 81 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 123 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ef14791e2da..515ae59cc0a 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2376,6 +2376,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProc->pgprocno)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3640,6 +3666,56 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if (buf_refcount == 1)
+ {
+ buf_state &= ~BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -4917,10 +4993,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 3bdc5f7fb6c..d71fe63d99b 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -788,14 +788,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -831,35 +838,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 166524c50e0..16e06cc9175 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -236,6 +236,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
#ifdef NOT_USED
extern void PrintPinnedBufs(void);
#endif
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6-REL_17_STABLE-0001-Fix-deadlock-detector-activation-in.patch (text/x-patch, 8.6K) download
From 90f95eef0777dafbcada78a75014e6a6b34cf555 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6-REL_17_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 81 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 123 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1f2bbcf24b0..d35a8a475fe 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2801,6 +2801,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4072,6 +4098,56 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ buf_state &= ~BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -5342,10 +5418,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 872679ca447..5bac5e8e351 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -787,14 +787,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -830,35 +837,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 09c43dd9c72..d0d26ef5f65 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -286,6 +286,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
#ifdef NOT_USED
extern void PrintPinnedBufs(void);
#endif
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6-REL_18_STABLE-0001-Fix-deadlock-detector-activation-in.patch (text/x-patch, 8.6K) download
From e5e61b582b08350360292928ef8525ca33d433d7 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6-REL_18_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 81 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 123 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 27fd7e9720a..9addd202f2d 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3258,6 +3258,32 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf, buf_state);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4517,6 +4543,56 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ buf_state &= ~BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -5750,10 +5826,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..924cfd671a1 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -788,14 +788,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -831,35 +838,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 41fdc1e7693..b30723d8129 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -285,6 +285,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-25 12:14 Fujii Masao <masao.fujii@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Fujii Masao @ 2026-06-25 12:14 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org
On Thu, Jun 25, 2026 at 12:16 AM Vitaly Davydov
<v.davydov@postgrespro.ru> wrote:
>
> Dear Fujii Masao
>
> I've prepared patches of v6 for other majors. I hope, it will help.
Thanks for the patches for the stable branches!
I noticed that you added "buf_state &= ~BM_PIN_COUNT_WAITER;" to
BufferIsReadyForCleanup(). Could you tell me why that's necessary? My
understanding was that LockBufferForCleanup() is responsible for
clearing that flag, so I didn't think it was needed here.
But if it is necessary, I'd like to make the same change in master as
well, to keep the code as consistent as possible between master and
the stable branches.
> I will also think of a better approach for tap-test patch as well.
Thanks!
Regards,
--
Fujii Masao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-25 13:36 Xuneng Zhou <xunengzhou@gmail.com>
parent: Fujii Masao <masao.fujii@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-06-25 13:36 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: Vitaly Davydov <v.davydov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org
Hi Fujii-san, Vitaly,
On Thu, Jun 25, 2026 at 8:14 PM Fujii Masao <masao.fujii@gmail.com> wrote:
>
> On Thu, Jun 25, 2026 at 12:16 AM Vitaly Davydov
> <v.davydov@postgrespro.ru> wrote:
> >
> > Dear Fujii Masao
> >
> > I've prepared patches of v6 for other majors. I hope, it will help.
Thanks for working on this. I haven't looked into the patch in detail
yet. The overall direction seems good to me. Here are two comments on
the first look:
1) It looks that this patch is doing some refactoring around
LockBufferForCleanup. The part being touched seems flaky itself on
HEAD, should we fix the issue in the buffer side first? [1]
> Thanks for the patches for the stable branches!
>
> I noticed that you added "buf_state &= ~BM_PIN_COUNT_WAITER;" to
> BufferIsReadyForCleanup(). Could you tell me why that's necessary? My
> understanding was that LockBufferForCleanup() is responsible for
> clearing that flag, so I didn't think it was needed here.
>
> But if it is necessary, I'd like to make the same change in master as
> well, to keep the code as consistent as possible between master and
> the stable branches.
2) buf_state &= ~BM_PIN_COUNT_WAITER in stable versions seems not
necessary to me as LockBufferForCleanup will handle the clearing for
us.
[1] https://www.postgresql.org/message-id/flat/7685519a-0bf9-4e17-93ca-7e3aa10fa29c%40gmail.com
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-26 09:31 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-06-26 09:31 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; Fujii Masao <masao.fujii@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Hi Fujii-san, Xuneng Zhou,
> 1) It looks that this patch is doing some refactoring around
> LockBufferForCleanup. The part being touched seems flaky itself on
> HEAD, should we fix the issue in the buffer side first? [1]
Thank you for mentioning it. It seems, there is a conflict with
the another patch due to the introduction of RegisterPinCountWaiter
function, that can not be resolved automatically. I'm ok to wait for
the conflicting fix to be released. It seems, our patch should be
reconsidered after the conflicting patch release.
> 2) buf_state &= ~BM_PIN_COUNT_WAITER in stable versions seems not
> necessary to me as LockBufferForCleanup will handle the clearing for
> us.
It seems it was my mistake when I resolved conflicts when cherry-picking
the changes from other branches. I removed this line in my recent v 6.1
attached patches. Thank you for catching it.
With best regards,
Vitaly
Attachments: download all (5 files, .tar.gz)
v6.1-REL_14_STABLE-0001-Fix-deadlock-detector-activation-.patch (text/x-patch, 8.6K) download
From 56953eb58463d88753ad4c907a1aa6280620a44a Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6.1-REL_14_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 71 +++++++++++++++----------
src/include/storage/bufmgr.h | 1 +
3 files changed, 122 insertions(+), 30 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f563742ad44..03774e8b57f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1839,6 +1839,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pid == MyProcPid);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pid != MyProcPid)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pid = MyProcPid;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3119,6 +3145,55 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pid == MyProcPid);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelFileNodeBuffers
*
@@ -4219,10 +4294,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pid = MyProcPid;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 49cccad9bf6..cb87f0b1fe1 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -749,14 +749,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -792,36 +799,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(
- PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 85caac3253d..3ef5dc7e349 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -223,6 +223,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void PrintPinnedBufs(void);
#endif
extern Size BufferShmemSize(void);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileNode *rnode,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6.1-REL_15_STABLE-0001-Fix-deadlock-detector-activation-.patch (text/x-patch, 8.6K) download
From 1b57ccaa48c2a1d92547c212c98d1c22bc032f33 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6.1-REL_15_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 122 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index a02814a8f51..0034f08e658 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1847,6 +1847,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProc->pgprocno)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3102,6 +3128,55 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelFileNodeBuffers
*
@@ -4356,10 +4431,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index a36eb80e9ae..2b029cdcf10 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -750,14 +750,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -793,35 +800,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 8e93d315a67..d0a92541ed8 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -226,6 +226,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void PrintPinnedBufs(void);
#endif
extern Size BufferShmemSize(void);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileNode *rnode,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6.1-REL_16_STABLE-0001-Fix-deadlock-detector-activation-.patch (text/x-patch, 8.6K) download
From 6dce6c08e5defabac9274d727bd758f3a3675daa Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6.1-REL_16_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 122 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ef14791e2da..385ca09bbe6 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2376,6 +2376,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProc->pgprocno)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -3640,6 +3666,55 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProc->pgprocno);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -4917,10 +4992,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProc->pgprocno;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 3bdc5f7fb6c..d71fe63d99b 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -788,14 +788,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -831,35 +838,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(PG_WAIT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 166524c50e0..16e06cc9175 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -236,6 +236,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
#ifdef NOT_USED
extern void PrintPinnedBufs(void);
#endif
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6.1-REL_17_STABLE-0001-Fix-deadlock-detector-activation-.patch (text/x-patch, 8.6K) download
From 1ccf9f9c6df32f19dda640499aa01b589efe1b53 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6.1-REL_17_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 122 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1f2bbcf24b0..91f007726e2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2801,6 +2801,32 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4072,6 +4098,55 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -5342,10 +5417,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 872679ca447..5bac5e8e351 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -787,14 +787,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -830,35 +837,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 09c43dd9c72..d0d26ef5f65 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -286,6 +286,7 @@ extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
#ifdef NOT_USED
extern void PrintPinnedBufs(void);
#endif
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
v6.1-REL_18_STABLE-0001-Fix-deadlock-detector-activation-.patch (text/x-patch, 8.6K) download
From aad3645c38d2b619c2c36c5b6a4bbf198de6a2e7 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v6.1-REL_18_STABLE] Fix deadlock detector activation in a
recovery conflict
When the startup process in a deadlock with a backend, it sends the
signal to the backend to trigger the deadlock detector when
the deadlock timeout is elapsed (deadlock_timeout guc). Due to some
optimization in timeout.c, when spontaneous SIGALRM signals are
possible, which doesn't relate to any enabled timeout, the function
ResolveRecoveryConflictWithBufferPin can never send the signal to the
conflicting backend, becase the deadlock timeout will never be
triggered.
The patch fixes ResolveRecoveryConflictWithBufferPin by ignoring
spontaneous SIGALRM signals, that are possible in the current
implementation of timeout.c functionality.
---
src/backend/storage/buffer/bufmgr.c | 80 +++++++++++++++++++++++++++--
src/backend/storage/ipc/standby.c | 70 ++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 122 insertions(+), 29 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 27fd7e9720a..6e792330cee 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3258,6 +3258,32 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf, buf_state);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ buf_state |= BM_PIN_COUNT_WAITER;
+ UnlockBufHdr(bufHdr, buf_state);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4517,6 +4543,55 @@ BufferGetLSNAtomic(Buffer buffer)
return lsn;
}
+/*
+ * BufferIsReadyForCleanup
+ * Recheck whether the startup process can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+BufferIsReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+ Assert(buf_refcount > 0);
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ return true;
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
@@ -5750,10 +5825,7 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
- buf_state |= BM_PIN_COUNT_WAITER;
- UnlockBufHdr(bufHdr, buf_state);
+ RegisterPinCountWaiter(bufHdr, buf_state);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..924cfd671a1 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -788,14 +788,21 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
if (GetCurrentTimestamp() >= ltime && ltime != 0)
@@ -831,35 +838,48 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
+ */
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
*/
- SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ if (BufferIsReadyForCleanup(bufid + 1))
+ break;
+
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 41fdc1e7693..b30723d8129 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -285,6 +285,7 @@ extern void DropDatabaseBuffers(Oid dbid);
extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
+extern bool BufferIsReadyForCleanup(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-27 06:11 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-06-27 06:11 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
On Fri, Jun 26, 2026 at 5:31 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Hi Fujii-san, Xuneng Zhou,
>
> > 1) It looks that this patch is doing some refactoring around
> > LockBufferForCleanup. The part being touched seems flaky itself on
> > HEAD, should we fix the issue in the buffer side first? [1]
>
> Thank you for mentioning it. It seems, there is a conflict with
> the another patch due to the introduction of RegisterPinCountWaiter
> function, that can not be resolved automatically. I'm ok to wait for
> the conflicting fix to be released. It seems, our patch should be
> reconsidered after the conflicting patch release.
OK.
> > 2) buf_state &= ~BM_PIN_COUNT_WAITER in stable versions seems not
> > necessary to me as LockBufferForCleanup will handle the clearing for
> > us.
>
> It seems it was my mistake when I resolved conflicts when cherry-picking
> the changes from other branches. I removed this line in my recent v 6.1
> attached patches. Thank you for catching it.
Thanks.
Here are some additional comments after further review:
3) should we let RegisterPinCountWaiter do less?
Currently, the function does a lot like:
RegisterPinCountWaiter()
{
check other waiter;
maybe unlock;
maybe elog(ERROR);
register;
unlock header;
}
We need to handle the unlock & elog for two
callers(BufferIsReadyForCleanup and LockBufferForCleanup) with
different needs, which could be error-prone sometimes.
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
For example, unlocking the header lock only is sufficient for
BufferIsReadyForCleanup but not for LockBufferForCleanup as we hold
the extra content lock. For LockBufferForCleanup, the check seems
duplicated as we already did it before invoking this function. So to
make this work, we may need to distinguish these two callers.
Another potential issue of doing safe checks here is the order of
assert and error out:
In assert build, if the condition is true, then the header lock and
content lock would not be released properly. So we may need to adjust
the order.
This leads me to wonder -- would it be better to let this help
function to do less, like just for registration and let the callers to
handle others.
4) the already-expired standby-delay path may not wake new pin holders
If ltime has already passed, the patch sends
RECOVERY_CONFLICT_BUFFERPIN once before entering the loop, then waits
inside the new loop. What if another backend pins the buffer after
that broadcast?
Before the patch, this seems fine because we return to
BufferIsReadyForCleanup and enter the function
ResolveRecoveryConflictWithBufferPin to do the re-check of ltime and
send the conflict signal once more.
With the new inner loop, BufferIsReadyForCleanup() can re-register and
continue waiting, but no timeout is active and no new cancelling
request is sent. So recovery can continue waiting for a buffer pin
even though max_standby_*_delay has already expired.
To fix this, should we add an in-loop check: if ltime != 0 &&
GetCurrentTimestamp() >= ltime in here:
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
5) The name of BufferIsReadyForCleanup seems broader than the actual protocol.
The function name seems not align with the intended use of it as the
comment suggested:
*
* This is only for the hot-standby path in LockBufferForCleanup(), via
* ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
* The caller must already be registered as the shared buffer's
* BM_PIN_COUNT_WAITER.
Would it be better to rename it to something like
RecheckCleanupLockPinWait or RecheckBufferPinWaiter?
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-06-30 03:14 Xuneng Zhou <xunengzhou@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-06-30 03:14 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
On Sat, Jun 27, 2026 at 2:11 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
>
> On Fri, Jun 26, 2026 at 5:31 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
> >
> > Hi Fujii-san, Xuneng Zhou,
> >
> > > 1) It looks that this patch is doing some refactoring around
> > > LockBufferForCleanup. The part being touched seems flaky itself on
> > > HEAD, should we fix the issue in the buffer side first? [1]
> >
> > Thank you for mentioning it. It seems, there is a conflict with
> > the another patch due to the introduction of RegisterPinCountWaiter
> > function, that can not be resolved automatically. I'm ok to wait for
> > the conflicting fix to be released. It seems, our patch should be
> > reconsidered after the conflicting patch release.
>
> OK.
>
> > > 2) buf_state &= ~BM_PIN_COUNT_WAITER in stable versions seems not
> > > necessary to me as LockBufferForCleanup will handle the clearing for
> > > us.
> >
> > It seems it was my mistake when I resolved conflicts when cherry-picking
> > the changes from other branches. I removed this line in my recent v 6.1
> > attached patches. Thank you for catching it.
>
> Thanks.
>
> Here are some additional comments after further review:
>
> 3) should we let RegisterPinCountWaiter do less?
>
> Currently, the function does a lot like:
> RegisterPinCountWaiter()
> {
> check other waiter;
> maybe unlock;
> maybe elog(ERROR);
> register;
> unlock header;
> }
>
> We need to handle the unlock & elog for two
> callers(BufferIsReadyForCleanup and LockBufferForCleanup) with
> different needs, which could be error-prone sometimes.
>
> + Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
> + bufHdr->wait_backend_pgprocno == MyProcNumber);
> +
> + if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
> + bufHdr->wait_backend_pgprocno != MyProcNumber)
> + {
> + UnlockBufHdr(bufHdr);
> + elog(ERROR, "multiple processes attempting to wait for pincount 1");
> + }
> +
>
> For example, unlocking the header lock only is sufficient for
> BufferIsReadyForCleanup but not for LockBufferForCleanup as we hold
> the extra content lock. For LockBufferForCleanup, the check seems
> duplicated as we already did it before invoking this function. So to
> make this work, we may need to distinguish these two callers.
>
> Another potential issue of doing safe checks here is the order of
> assert and error out:
> In assert build, if the condition is true, then the header lock and
> content lock would not be released properly. So we may need to adjust
> the order.
>
> This leads me to wonder -- would it be better to let this help
> function to do less, like just for registration and let the callers to
> handle others.
>
> 4) the already-expired standby-delay path may not wake new pin holders
>
> If ltime has already passed, the patch sends
> RECOVERY_CONFLICT_BUFFERPIN once before entering the loop, then waits
> inside the new loop. What if another backend pins the buffer after
> that broadcast?
>
> Before the patch, this seems fine because we return to
> BufferIsReadyForCleanup and enter the function
> ResolveRecoveryConflictWithBufferPin to do the re-check of ltime and
> send the conflict signal once more.
>
> With the new inner loop, BufferIsReadyForCleanup() can re-register and
> continue waiting, but no timeout is active and no new cancelling
> request is sent. So recovery can continue waiting for a buffer pin
> even though max_standby_*_delay has already expired.
>
> To fix this, should we add an in-loop check: if ltime != 0 &&
> GetCurrentTimestamp() >= ltime in here:
>
> + if (got_standby_delay_timeout)
> + {
> + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
> + break;
> + }
>
> 5) The name of BufferIsReadyForCleanup seems broader than the actual protocol.
>
> The function name seems not align with the intended use of it as the
> comment suggested:
>
> *
> * This is only for the hot-standby path in LockBufferForCleanup(), via
> * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
> * The caller must already be registered as the shared buffer's
> * BM_PIN_COUNT_WAITER.
>
> Would it be better to rename it to something like
> RecheckCleanupLockPinWait or RecheckBufferPinWaiter?
I took another look at the patch. Here is an another thought:
6) We might want to be more elaborative on the commit message
From personal experience, the message seems a bit hard to follow if
one does not read the related discussion. I like your analysis of the
problem at the start of this thread [1]. Would it be sensible to
summarize and reuse some part of it for the message?
The motivation of this patch is to fix deadlock detector activation,
but the wait-loop change has a slightly broader benefit. It also
improves the standby timeout path indirectly by avoiding needless
timeout teardown and re-entry on unrelated wakeups, and by keeping any
already-fired timeout state until the loop handles it. Should we
mention this as an additional benefit, or even frame the message
broader like:
Fix the buffer-pin recovery-conflict wait loop.
[1] https://www.postgresql.org/message-id/44c24dcf-5710-410f-b1b6-d10b315f3d51%40postgrespro.ru
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-07-01 15:57 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 2 replies; 43+ messages in thread
From: Vitaly Davydov @ 2026-07-01 15:57 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
Dear Xuneng Zhou
Thank you very much for such a comprehensive feedback.
On 6/30/26 06:14, Xuneng Zhou wrote:
3)
> Another potential issue of doing safe checks here is the order of
> assert and error out:
> In assert build, if the condition is true, then the header lock and
> content lock would not be released properly. So we may need to adjust
> the order.
> This leads me to wonder -- would it be better to let this help
> function to do less, like just for registration and let the callers to
> handle others.
I agree with your comment. Furthermore, I do not like the approach with
locking and unlocking in different functions. But, I see this approach
is used in some other places in the code. I should think how to improve it.
I have some doubts about the code where LockBufferForCleanup (bufmgr.c)
calls ResolveRecoveryConflictWithBufferPin (standby.c), but the latter
function calls BufferIsReadyForCleanup (bufmgr.c). There is an idea to
refactor it in the future, because these functions are closely coupled.
May be unite them or move ResolveRecoveryConflictWithBufferPin into
bufmgr.c...
4) the already-expired standby-delay path may not wake new pin holders
Agree, thank you. It seems it is a kind of an optimization to avoid
activation of timeouts. Is it enought to return after
SendRecoveryConflictWithBufferPin, once we sent a signal to a conflicting
backend? Not sure, we have to wait with ProcWaitForSignal. Of course, the
same check should be in the loop, when timeouts are already activated. If
another backend pins the buffer after the broadcast, it will be handled
in the parent function.
5) The name of BufferIsReadyForCleanup seems broader than the actual protocol.
What about to move RegisterPinCountWaiter out of this function? May be, rename
it into BufferGetRefCount() or something else like it? In this case, its
semantics will be simple and well-defined, and it can be re-used in some other
places. Another thing is to add a secong function argument like
doRegisterWaiter.
We may also remove BufferIsReadyForCleanup but put its code into
ResolveRecoveryConflictWithBufferPin.
I have no good solution right now. Let me please think about it.
6)
> From personal experience, the message seems a bit hard to follow if
> one does not read the related discussion. I like your analysis of the
> problem at the start of this thread [1]. Would it be sensible to
> summarize and reuse some part of it for the message?
> The motivation of this patch is to fix deadlock detector activation,
> but the wait-loop change has a slightly broader benefit. It also
> improves the standby timeout path indirectly by avoiding needless
> timeout teardown and re-entry on unrelated wakeups, and by keeping any
> already-fired timeout state until the loop handles it. Should we
> mention this as an additional benefit, or even frame the message
> broader like:
> Fix the buffer-pin recovery-conflict wait loop.
Thank you for such a detailed comment. I would like to propose the following
commit message:
Fix recovery-conflict wait loop for buffer pins on hot standby
When a deadlock occurs between the startup process and a backend during
recovery on a hot standby, the deadlock detector may fail to activate if
deadlock_timeout exceeds log_startup_progress_interval. Due to an
optimization in timeout.c that avoids redundant setitimer() calls,
periodic SIGALRM signals from the progress timer can arrive before the
deadlock timeout expires. These signals wake the startup process
prematurely, causing ResolveRecoveryConflictWithBufferPin to reset all
timeouts and retry without ever triggering the deadlock detector. This
results in an infinite loop in LockBufferForCleanup and stalls recovery
indefinitely, particularly when max_standby_streaming_delay = -1.
The timeout.c subsystem uses an optimization that avoids redundant
setitimer() calls when the nearest active timeout's expiration time is
later than the currently armed timer. As a result, a SIGALRM from a
previously set timer (e.g., log_startup_progress_interval) can fire when
no currently active timeout has actually been reached. This periodic
signal wakes ResolveRecoveryConflictWithBufferPin prematurely, resetting
the timeout loop without ever triggering the deadlock detector at
deadlock_timeout. The deadlock timeout is perpetually re-armed but never
fires because each periodic SIGALRM arrives before it expires.
This patch fixes the issue by making ResolveRecoveryConflictWithBufferPin
ignore unintended SIGALRM signals that do not correspond to any currently
enabled timeout, ensuring the deadlock timeout fires correctly and the
conflicting backend is signaled to check for deadlocks.
It improves the standby timeout path by reducing unnecessary timeout
teardown and re-arming on unrelated wakeups, preserving the activated
timer state until the wait loop completes. This makes the deadlock
timeout behavior consistent with user expectations.
It seems, the patch should be completely rewritten. I will try to
prepare a new patch version.
With best regards,
Vitaly
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-07-02 09:31 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
1 sibling, 0 replies; 43+ messages in thread
From: Xuneng Zhou @ 2026-07-02 09:31 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
On Wed, Jul 1, 2026 at 11:58 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Dear Xuneng Zhou
>
> Thank you very much for such a comprehensive feedback.
>
> On 6/30/26 06:14, Xuneng Zhou wrote:
>
> 3)
>
> > Another potential issue of doing safe checks here is the order of
> > assert and error out:
> > In assert build, if the condition is true, then the header lock and
> > content lock would not be released properly. So we may need to adjust
> > the order.
>
> > This leads me to wonder -- would it be better to let this help
> > function to do less, like just for registration and let the callers to
> > handle others.
>
> I agree with your comment. Furthermore, I do not like the approach with
> locking and unlocking in different functions. But, I see this approach
> is used in some other places in the code. I should think how to improve it.
>
> I have some doubts about the code where LockBufferForCleanup (bufmgr.c)
> calls ResolveRecoveryConflictWithBufferPin (standby.c), but the latter
> function calls BufferIsReadyForCleanup (bufmgr.c). There is an idea to
> refactor it in the future, because these functions are closely coupled.
> May be unite them or move ResolveRecoveryConflictWithBufferPin into
> bufmgr.c...
I agree that letting the resolver own the wait for loop is not very
ideal. The ideal design would be the code that owns the wait predicate
owns the wait loop(bufmgr). But there seems no easy way to do it.
> 4) the already-expired standby-delay path may not wake new pin holders
>
> Agree, thank you. It seems it is a kind of an optimization to avoid
> activation of timeouts. Is it enought to return after
> SendRecoveryConflictWithBufferPin, once we sent a signal to a conflicting
> backend? Not sure, we have to wait with ProcWaitForSignal. Of course, the
> same check should be in the loop, when timeouts are already activated. If
> another backend pins the buffer after the broadcast, it will be handled
> in the parent function.
Yeah, returning right away after the initial
SendRecoveryConflictWithBufferPin() is a possible way to fix the
issue, but it may cause repeated broadcasts / busy retrying with the
current design.
> 5) The name of BufferIsReadyForCleanup seems broader than the actual protocol.
>
> What about to move RegisterPinCountWaiter out of this function? May be, rename
> it into BufferGetRefCount() or something else like it? In this case, its
> semantics will be simple and well-defined, and it can be re-used in some other
> places. Another thing is to add a secong function argument like
> doRegisterWaiter.
>
> We may also remove BufferIsReadyForCleanup but put its code into
> ResolveRecoveryConflictWithBufferPin.
>
> I have no good solution right now. Let me please think about it.
>
> 6)
>
> > From personal experience, the message seems a bit hard to follow if
> > one does not read the related discussion. I like your analysis of the
> > problem at the start of this thread [1]. Would it be sensible to
> > summarize and reuse some part of it for the message?
>
> > The motivation of this patch is to fix deadlock detector activation,
> > but the wait-loop change has a slightly broader benefit. It also
> > improves the standby timeout path indirectly by avoiding needless
> > timeout teardown and re-entry on unrelated wakeups, and by keeping any
> > already-fired timeout state until the loop handles it. Should we
> > mention this as an additional benefit, or even frame the message
> > broader like:
> > Fix the buffer-pin recovery-conflict wait loop.
>
> Thank you for such a detailed comment. I would like to propose the following
> commit message:
This version seems better to me in general.
> Fix recovery-conflict wait loop for buffer pins on hot standby
>
> When a deadlock occurs between the startup process and a backend during
> recovery on a hot standby, the deadlock detector may fail to activate if
> deadlock_timeout exceeds log_startup_progress_interval. Due to an
> optimization in timeout.c that avoids redundant setitimer() calls,
> periodic SIGALRM signals from the progress timer can arrive before the
> deadlock timeout expires. These signals wake the startup process
> prematurely, causing ResolveRecoveryConflictWithBufferPin to reset all
> timeouts and retry without ever triggering the deadlock detector. This
> results in an infinite loop in LockBufferForCleanup and stalls recovery
> indefinitely, particularly when max_standby_streaming_delay = -1.
>
> The timeout.c subsystem uses an optimization that avoids redundant
> setitimer() calls when the nearest active timeout's expiration time is
> later than the currently armed timer. As a result, a SIGALRM from a
> previously set timer (e.g., log_startup_progress_interval) can fire when
> no currently active timeout has actually been reached. This periodic
> signal wakes ResolveRecoveryConflictWithBufferPin prematurely, resetting
> the timeout loop without ever triggering the deadlock detector at
> deadlock_timeout. The deadlock timeout is perpetually re-armed but never
> fires because each periodic SIGALRM arrives before it expires.
These two paras seem repetitive to state the same thing. We might want
to consolidate them. Another thing is should we focus less on the
specific issue of log_startup_progress_interval? It's one of the
sources that generate unrelated wake-ups that lead to this issue. The
deeper issue is the buffer-pin wait code treats any latch wakeup as if
it were a relevant buffer-pin wait event.
> This patch fixes the issue by making ResolveRecoveryConflictWithBufferPin
> ignore unintended SIGALRM signals that do not correspond to any currently
> enabled timeout, ensuring the deadlock timeout fires correctly and the
> conflicting backend is signaled to check for deadlocks.
Unintended SIGALRM signals are the thing we try to ignore. It looks
more like the end rather than the means. What we do seems broader: we
ignore all unrelated latch wake-ups and only react to the genuine
ready signal we care about.
> It improves the standby timeout path by reducing unnecessary timeout
> teardown and re-arming on unrelated wakeups, preserving the activated
> timer state until the wait loop completes. This makes the deadlock
> timeout behavior consistent with user expectations.
>
>
> It seems, the patch should be completely rewritten. I will try to
> prepare a new patch version.
Thanks for that.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-04 05:30 JoongHyuk Shin <sjh910805@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
1 sibling, 1 reply; 43+ messages in thread
From: JoongHyuk Shin @ 2026-08-04 05:30 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: Xuneng Zhou <xunengzhou@gmail.com>; Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
On Thu, Jul 2, 2026 at 12:57 AM Vitaly Davydov <v.davydov@postgrespro.ru>
wrote:
> It seems, the patch should be completely rewritten. I will try to
> prepare a new patch version.
Hi Vitaly,
Any update on the new version you mentioned in your last message?
I ask because my patch at [1] reworks the same dispatch block in
ResolveRecoveryConflictWithBufferPin, and I plan to rebase it on top of
your rewrite, so I would like to time that.
Happy to help with testing if that is useful.
[1]
https://www.postgresql.org/message-id/flat/CACSdjfO3ox8i8N3KxGf3HThzX1vnRRkY2kq1m5qsvGxjcuM1jg%40mai...
--
JH Shin
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-04 11:16 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: JoongHyuk Shin <sjh910805@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-08-04 11:16 UTC (permalink / raw)
To: JoongHyuk Shin <sjh910805@gmail.com>; +Cc: Xuneng Zhou <xunengzhou@gmail.com>; Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi JH Shin,
On 8/4/26 08:30, JoongHyuk Shin wrote:
> On Thu, Jul 2, 2026 at 12:57 AM Vitaly Davydov <v.davydov@postgrespro.ru
> <mailto:v.davydov@postgrespro.ru>> wrote:
>> It seems, the patch should be completely rewritten. I will try to
>> prepare a new patch version.
>
> Hi Vitaly,
>
> Any update on the new version you mentioned in your last message?
I can't give you an exact time right now because of the issues highlighted
in the last review that need to be elaborated on. Sorry, I've just started
looking at it again. It might take a week or two. If your patch is ready for
commit, please go ahead with it. As I see it, the main change in your patch
avoids periodic message logging by passing the log_recovery_conflict variable
from the caller. Once your changes are committed, I don't foresee any problems
merging them into my patch.
Vitaly Davydov
vitcpp@mail.ru
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-05 02:40 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-08-05 02:40 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: JoongHyuk Shin <sjh910805@gmail.com>; Fujii Masao <masao.fujii@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Aug 4, 2026 at 7:16 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Hi JH Shin,
>
> On 8/4/26 08:30, JoongHyuk Shin wrote:
> > On Thu, Jul 2, 2026 at 12:57 AM Vitaly Davydov <v.davydov@postgrespro.ru
> > <mailto:v.davydov@postgrespro.ru>> wrote:
> >> It seems, the patch should be completely rewritten. I will try to
> >> prepare a new patch version.
> >
> > Hi Vitaly,
> >
> > Any update on the new version you mentioned in your last message?
>
> I can't give you an exact time right now because of the issues highlighted
> in the last review that need to be elaborated on. Sorry, I've just started
> looking at it again. It might take a week or two. If your patch is ready for
> commit, please go ahead with it. As I see it, the main change in your patch
> avoids periodic message logging by passing the log_recovery_conflict variable
> from the caller. Once your changes are committed, I don't foresee any problems
> merging them into my patch.
Personally, I still think the patch by JoongHyuk should be deferred
until the one in the current thread is applied, because the imagined
race case [1] still seems possible to me in a refreshed look. That
potential race might be fixed in standalone beforehand but the
potential fix seems not elegant to me. Sure the decision of pulling
the trigger or not is up to Fujii-san or other committers. There seems
to be some additional threads/issues piled up in this area [2].
Haven't checked the dependency among them yet. I'd like to help if you
don't have time, but I won't be able to do so in this or next week
since I've been wrestling with some subtle bugs which better be
reported/fixed before the upcoming GA.
[1] https://www.postgresql.org/message-id/CABPTF7VCdsfmvZpvXtuNHYhBYK4ivnfPAAM=vFXsgqYv2p2eyA@mail.gmail...
[2] https://www.postgresql.org/message-id/CAHg+QDf3NcB3vOAqQ5EFcV5DYYUZ9snCSJAU-x1JDbtcjK3eBQ@mail.gmail...
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-05 08:10 Vitaly Davydov <v.davydov@postgrespro.ru>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-08-05 08:10 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Dear Xuneng Zhou, All
I've updated the patch for the master branch taking into account Xuneng Zhou's
review notes. Please, take a look.
Key notes are:
1. Renamed BufferIsReadyForCleanup to PinCountWaiterCheckReadyForCleanup and
removed some asserts just after LockBufHdr to avoid stalling of other processes
accessing this buffer if assert is activated. I'm not sure what are the adopted
rules for asserts - should we avoid indefinite locks or other issues due to
assert activation in general?
2. Removed elog(ERROR) from RegisterPinCountWaiter. I kept the asserts
unchanged but I'm not sure about it.
3. Fixed the issue with fast path in ResolveRecoveryConflictWithBufferPin. I
reused got_standby_delay_timeout variable in fast path when current time is
greater than ltime. There is no need to check this condition every time in
the for() loop. Checking the condition once and set got_standby_delay_timeout
is enough.
With best regards,
Vitaly
vitcpp@mail.ru (aka v.davydov@postgrespro.ru)
Attachments:
v7-0001-Fix-recovery-conflict-wait-loop-for-buffer-pins-o.patch (text/x-patch, 8.7K) download
From ae31fed6faa65045737a7912074865c5853cb7dc Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <v.davydov@postgrespro.ru>
Date: Wed, 10 Jun 2026 12:12:54 +0900
Subject: [PATCH v7] Fix recovery-conflict wait loop for buffer pins on hot
standby
When the startup process waits on a buffer pin during recovery, the
buffer-pin wait code treats any latch wakeup as a relevant event,
resetting all timeouts and retrying. On hot standby, periodic SIGALRM
signals from unrelated timers (such as progress reporting) can arrive
before the deadlock timeout expires, causing the wait loop to reset
indefinitely without ever triggering the deadlock detector. This results
in an infinite loop in LockBufferForCleanup and stalls recovery
indefinitely, particularly when max_standby_streaming_delay = -1.
---
src/backend/storage/buffer/bufmgr.c | 73 +++++++++++++++++++++++++
src/backend/storage/ipc/standby.c | 84 ++++++++++++++++++++---------
src/include/storage/bufmgr.h | 1 +
3 files changed, 132 insertions(+), 26 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 169829eb020..ed69896d431 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3469,6 +3469,25 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer,
+ * set the BM_PIN_COUNT_WAITER flag, and unlock the buffer header.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static void
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+ UnlockBufHdrExt(bufHdr, buf_state, BM_PIN_COUNT_WAITER, 0, 0);
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4763,6 +4782,60 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * PinCountWaiterCheckReadyForCleanup
+ * Recheck whether pin count waiter process (the startup process)
+ * can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+PinCountWaiterCheckReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again.
+ */
+ RegisterPinCountWaiter(bufHdr, buf_state);
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..044284b7ba5 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,22 +790,36 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
- if (GetCurrentTimestamp() >= ltime && ltime != 0)
+ if (ltime != 0 && GetCurrentTimestamp() >= ltime)
{
/*
* We're already behind, so clear a path as quickly as possible.
*/
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+
+ /*
+ * Set delay timeout flag once the timeout is reached (the current
+ * timestamp is greater than the standby limit time). This variable is
+ * use by timeout handlers with the same purpose.
+ */
+ got_standby_delay_timeout = true;
}
else
{
@@ -833,35 +847,53 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
+ */
+ if (PinCountWaiterCheckReadyForCleanup(bufid + 1))
+ break;
+
+ /*
+ * Send the recovery conflict if the standby delay timeout is activated or
+ * standby limit time was already reached. The second condition handles the
+ * fast path when timeouts are not activated.
+ */
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..cf8ef54aedc 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -313,6 +313,7 @@ extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
+extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std);
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-08 13:51 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <v.davydov@postgrespro.ru>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-08-08 13:51 UTC (permalink / raw)
To: Vitaly Davydov <v.davydov@postgrespro.ru>; +Cc: JoongHyuk Shin <sjh910805@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Hi Vitaly,
Thanks for updating the patch!
On Wed, Aug 5, 2026 at 4:10 PM Vitaly Davydov <v.davydov@postgrespro.ru> wrote:
>
> Dear Xuneng Zhou, All
>
> I've updated the patch for the master branch taking into account Xuneng Zhou's
> review notes. Please, take a look.
>
> Key notes are:
>
> 1. Renamed BufferIsReadyForCleanup to PinCountWaiterCheckReadyForCleanup and
> removed some asserts just after LockBufHdr to avoid stalling of other processes
> accessing this buffer if assert is activated. I'm not sure what are the adopted
> rules for asserts - should we avoid indefinite locks or other issues due to
> assert activation in general?
>
> 2. Removed elog(ERROR) from RegisterPinCountWaiter. I kept the asserts
> unchanged but I'm not sure about it.
>
> 3. Fixed the issue with fast path in ResolveRecoveryConflictWithBufferPin. I
> reused got_standby_delay_timeout variable in fast path when current time is
> greater than ltime. There is no need to check this condition every time in
> the for() loop. Checking the condition once and set got_standby_delay_timeout
> is enough.
I haven't looked into the details yet. This version seems not resolve
the awkwardness of dependency you pointed out earlier. Just wondering
the reason for not aiming higher. It seems not trivial but still
feasible. Do you think we better try to land the bug fix first and do
further refactoring based on that?
"I agree with your comment. Furthermore, I do not like the approach with
locking and unlocking in different functions. But, I see this approach
is used in some other places in the code. I should think how to improve it.
I have some doubts about the code where LockBufferForCleanup (bufmgr.c)
calls ResolveRecoveryConflictWithBufferPin (standby.c), but the latter
function calls BufferIsReadyForCleanup (bufmgr.c). There is an idea to
refactor it in the future, because these functions are closely coupled.
May be unite them or move ResolveRecoveryConflictWithBufferPin into
bufmgr.c..."
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-13 17:35 Vitaly Davydov <vitprof@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-08-13 17:35 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: JoongHyuk Shin <sjh910805@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Dear Xuneng Zhou, All
Sorry for the delay. Starting from this moment I will use my personal email
vitprof@gmail.com instead of vdavydov(at)postgrespro(dot)ru.
On 8/8/26 16:51, Xuneng Zhou wrote:
> I haven't looked into the details yet. This version seems not resolve
> the awkwardness of dependency you pointed out earlier. Just wondering
> the reason for not aiming higher. It seems not trivial but still
> feasible. Do you think we better try to land the bug fix first and do
> further refactoring based on that?
Yes, I haven't refactored the dependency. I think to fix the bug first
to continue progress, but refactor it later. I think the future
discussions about refactored versions may take a while.
> "I agree with your comment. Furthermore, I do not like the approach of
> locking and unlocking in different functions. But, I see this approach
> is used in some other places in the code. I should think about how to improve it.
>
> I have some doubts about the code where LockBufferForCleanup (bufmgr.c)
> calls ResolveRecoveryConflictWithBufferPin (standby.c), but the latter
> function calls BufferIsReadyForCleanup (bufmgr.c). There is an idea to
> refactor it in the future, because these functions are closely coupled.
> May be unite them or move ResolveRecoveryConflictWithBufferPin into
> bufmgr.c..."
I agree, it should be refactored. The problem is that BufferIsReadyForCleanup
uses some static variables from bufmgr.c. Moving
ResolveRecoveryConflictWithBufferPin
into bufmgr.c helps to break this dependency and looks good for me.
I have some thoughts about the term 'pin count waiter'. I think it is a
confusing name. There is an idea to name it as buffer writer, once
multiple-readers-single-writer lock is implemented here. Pin count waiter
is just another term for a buffer writer, I think.
I propose to go further with the current change to speed up the progress
because other contributors wait for it.
With best regards,
Vitaly Davydov
vitprof@gmail.com (aka v.davydov@postgrespro.ru)
vitcpp@mail.ru
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-17 13:51 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <vitprof@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-08-17 13:51 UTC (permalink / raw)
To: Vitaly Davydov <vitprof@gmail.com>; +Cc: JoongHyuk Shin <sjh910805@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
On Fri, Aug 14, 2026 at 1:35 AM Vitaly Davydov <vitprof@gmail.com> wrote:
>
> Dear Xuneng Zhou, All
>
> Sorry for the delay. Starting from this moment I will use my personal email
> vitprof@gmail.com instead of vdavydov(at)postgrespro(dot)ru.
>
> On 8/8/26 16:51, Xuneng Zhou wrote:
> > I haven't looked into the details yet. This version seems not resolve
> > the awkwardness of dependency you pointed out earlier. Just wondering
> > the reason for not aiming higher. It seems not trivial but still
> > feasible. Do you think we better try to land the bug fix first and do
> > further refactoring based on that?
>
> Yes, I haven't refactored the dependency. I think to fix the bug first
> to continue progress, but refactor it later. I think the future
> discussions about refactored versions may take a while.
>
> > "I agree with your comment. Furthermore, I do not like the approach of
> > locking and unlocking in different functions. But, I see this approach
> > is used in some other places in the code. I should think about how to improve it.
> >
> > I have some doubts about the code where LockBufferForCleanup (bufmgr.c)
> > calls ResolveRecoveryConflictWithBufferPin (standby.c), but the latter
> > function calls BufferIsReadyForCleanup (bufmgr.c). There is an idea to
> > refactor it in the future, because these functions are closely coupled.
> > May be unite them or move ResolveRecoveryConflictWithBufferPin into
> > bufmgr.c..."
>
> I agree, it should be refactored. The problem is that BufferIsReadyForCleanup
> uses some static variables from bufmgr.c. Moving
> ResolveRecoveryConflictWithBufferPin
> into bufmgr.c helps to break this dependency and looks good for me.
>
> I have some thoughts about the term 'pin count waiter'. I think it is a
> confusing name. There is an idea to name it as buffer writer, once
> multiple-readers-single-writer lock is implemented here. Pin count waiter
> is just another term for a buffer writer, I think.
>
> I propose to go further with the current change to speed up the progress
> because other contributors wait for it.
+1 for the maneuver. I'll review the latest offering.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-27 10:35 JoongHyuk Shin <sjh910805@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: JoongHyuk Shin @ 2026-08-27 10:35 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: Vitaly Davydov <vitprof@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
On Wed, Aug 5, 2026 at 11:41 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
> Personally, I still think the patch by JoongHyuk should be deferred
> until the one in the current thread is applied,
Agreed.
On Fri, Aug 14, 2026 at 2:35 AM Vitaly Davydov <vitprof@gmail.com> wrote:
> I propose to go further with the current change to speed up the progress
> because other contributors wait for it.
If any of that is on my account, please don't worry about it.
I'm in no rush, and waiting costs me nothing. I'd rather it land
the way you both think is right, even if it takes longer.
--
JH Shin
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-28 08:17 JoongHyuk Shin <sjh910805@gmail.com>
parent: JoongHyuk Shin <sjh910805@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: JoongHyuk Shin @ 2026-08-28 08:17 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: Vitaly Davydov <vitprof@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Hi Vitaly,
I found one thing while reviewing v7.
I think PinCountWaiterCheckReadyForCleanup() has a window between
checking the refcount and setting BM_PIN_COUNT_WAITER. On the master
branch, UnpinBufferNoOwner() drops pins without taking the header
lock, so the last other pin can go away in that window and the
wakeup is lost until the deadlock or standby-limit timeout fires.
This looks like the same race that commit 8d85cb889a3 fixed in
LockBufferForCleanup(), and the same fix should work here. After
publishing the flag, recheck the refcount, and if only our own pin
remains, clear the flag and return true.
--
JH Shin
>
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-31 16:18 Vitaly Davydov <vitprof@gmail.com>
parent: JoongHyuk Shin <sjh910805@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-08-31 16:18 UTC (permalink / raw)
To: JoongHyuk Shin <sjh910805@gmail.com>; Xuneng Zhou <xunengzhou@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Hi JH Shin,
> I think PinCountWaiterCheckReadyForCleanup() has a window between
> checking the refcount and setting BM_PIN_COUNT_WAITER. On the master
> branch, UnpinBufferNoOwner() drops pins without taking the header
> lock, so the last other pin can go away in that window and the
> wakeup is lost until the deadlock or standby-limit timeout fires.
> This looks like the same race that commit 8d85cb889a3 fixed in
> LockBufferForCleanup(), and the same fix should work here. After
> publishing the flag, recheck the refcount, and if only our own pin
> remains, clear the flag and return true.
Agree, thank you.
I made new changes in two commits. In the first commit coming from v7 patch
I modified RegisterPinCountWaiter() function to recheck the refcount as
explained by you. In the second commit I've replaced the duplicate code in
LockBufferForCleanup to register pincount waiter with the call of
RegisterPinCountWaiter(). The second commit is optional but it fits well
with
the current changes.
The patch was rebased to the actual master branch.
P.S. I replaced my old address v.davydov@postgrespro.ru in commits with
my personal email vitprof@gmail.com that suits better for me.
With best regards,
Vitaly Davydov
vitprof@gmail.com (aka v.davydov@postgrespro.ru)
vitcpp@mail.ru
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-08-31 16:27 Vitaly Davydov <vitprof@gmail.com>
parent: Vitaly Davydov <vitprof@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-08-31 16:27 UTC (permalink / raw)
To: JoongHyuk Shin <sjh910805@gmail.com>; Xuneng Zhou <xunengzhou@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Sorry for missed patches. Attached now.
On 8/31/26 19:18, Vitaly Davydov wrote:
> Hi JH Shin,
>
> > I think PinCountWaiterCheckReadyForCleanup() has a window between
> > checking the refcount and setting BM_PIN_COUNT_WAITER. On the master
> > branch, UnpinBufferNoOwner() drops pins without taking the header
> > lock, so the last other pin can go away in that window and the
> > wakeup is lost until the deadlock or standby-limit timeout fires.
> > This looks like the same race that commit 8d85cb889a3 fixed in
> > LockBufferForCleanup(), and the same fix should work here. After
> > publishing the flag, recheck the refcount, and if only our own pin
> > remains, clear the flag and return true.
>
> Agree, thank you.
>
> I made new changes in two commits. In the first commit coming from v7
> patch
> I modified RegisterPinCountWaiter() function to recheck the refcount as
> explained by you. In the second commit I've replaced the duplicate
> code in
> LockBufferForCleanup to register pincount waiter with the call of
> RegisterPinCountWaiter(). The second commit is optional but it fits
> well with
> the current changes.
>
> The patch was rebased to the actual master branch.
>
> P.S. I replaced my old address v.davydov@postgrespro.ru in commits with
> my personal email vitprof@gmail.com that suits better for me.
With best regards,
Vitaly Davydov
vitprof@gmail.com (aka v.davydov@postgrespro.ru)
vitcpp@mail.ru
Attachments: download all (2 files, .tar.gz)
v8-0001-Fix-recovery-conflict-wait-loop-for-buffer-pins-o.patch (text/x-patch, 9.8K) download
From 167415c2bc7f2792e2a2357985c2007f1d2c4268 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <vitprof@gmail.com>
Date: Mon, 31 Aug 2026 18:38:15 +0300
Subject: [PATCH v8 1/2] Fix recovery-conflict wait loop for buffer pins on hot
standby
When the startup process waits on a buffer pin during recovery, the
buffer-pin wait code treats any latch wakeup as a relevant event,
resetting all timeouts and retrying. On hot standby, periodic SIGALRM
signals from unrelated timers (such as progress reporting) can arrive
before the deadlock timeout expires, causing the wait loop to reset
indefinitely without ever triggering the deadlock detector. This results
in an infinite loop in LockBufferForCleanup and stalls recovery
indefinitely, particularly when max_standby_streaming_delay = -1.
---
src/backend/storage/buffer/bufmgr.c | 106 ++++++++++++++++++++++++++++
src/backend/storage/ipc/standby.c | 84 +++++++++++++++-------
src/include/storage/bufmgr.h | 1 +
3 files changed, 165 insertions(+), 26 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 17f142e4c5b..686265c3d48 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3469,6 +3469,54 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer
+ * and unlock the buffer header. Return true if the process was registered as
+ * the pincount waiter and must now wait to be signaled, or false if the
+ * shared refcount was concurrently reduced to 1 (only our own pin remains),
+ * in which case no wait is necessary.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static bool
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+
+ /*
+ * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
+ * The shared refcount can be decremented while BM_LOCKED is set, so
+ * use an atomic operation that preserves concurrent refcount changes.
+ */
+ pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
+
+ /*
+ * Recheck the refcount after publishing the waiter flag, while shared
+ * refcount increments are still prevented by BM_LOCKED. If only our
+ * pin remains, the cleanup-lock condition has already been satisfied,
+ * so remove the waiter state and return without sleeping.
+ */
+ buf_state = pg_atomic_read_u64(&bufHdr->state);
+ if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
+ {
+ UnlockBufHdrExt(bufHdr, buf_state,
+ 0, BM_PIN_COUNT_WAITER,
+ 0);
+ PinCountWaitBuf = NULL;
+ return false;
+ }
+
+ UnlockBufHdr(bufHdr);
+
+ return true;
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4763,6 +4811,64 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * PinCountWaiterCheckReadyForCleanup
+ * Recheck whether pin count waiter process (the startup process)
+ * can retry cleanup lock acquisition.
+ *
+ * This is only for the hot-standby path in LockBufferForCleanup(), via
+ * ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
+ * The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
+ *
+ * Returns true when the caller itself is the only remaining pin holder, so it
+ * can retry taking the cleanup lock. Returns false if other backends still
+ * pin the shared buffer. In that case, this function guarantees that the
+ * current backend remains registered as the pincount waiter to be woken when
+ * the buffer refcount drops to 1.
+ */
+bool
+PinCountWaiterCheckReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again. The refcount may be concurrently
+ * reduced to 1 despite our holding the buffer header lock, in which case
+ * RegisterPinCountWaiter() returns false and the buffer is ready for
+ * cleanup.
+ */
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
+ return true;
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..044284b7ba5 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,22 +790,36 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
- if (GetCurrentTimestamp() >= ltime && ltime != 0)
+ if (ltime != 0 && GetCurrentTimestamp() >= ltime)
{
/*
* We're already behind, so clear a path as quickly as possible.
*/
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+
+ /*
+ * Set delay timeout flag once the timeout is reached (the current
+ * timestamp is greater than the standby limit time). This variable is
+ * use by timeout handlers with the same purpose.
+ */
+ got_standby_delay_timeout = true;
}
else
{
@@ -833,35 +847,53 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
+ */
+ if (PinCountWaiterCheckReadyForCleanup(bufid + 1))
+ break;
+
+ /*
+ * Send the recovery conflict if the standby delay timeout is activated or
+ * standby limit time was already reached. The second condition handles the
+ * fast path when timeouts are not activated.
+ */
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..cf8ef54aedc 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -313,6 +313,7 @@ extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
+extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std);
--
2.43.0
v8-0002-Use-RegisterPinCountWaiter-in-LockBufferForCleanu.patch (text/x-patch, 2.3K) download
From fe0e4c340101489c062cad43f59d7b3a01407718 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <vitprof@gmail.com>
Date: Mon, 31 Aug 2026 18:35:41 +0300
Subject: [PATCH v8 2/2] Use RegisterPinCountWaiter() in LockBufferForCleanup()
Replace the duplicated pincount-waiter registration logic in
LockBufferForCleanup() with a call to RegisterPinCountWaiter(), which
already encapsulates the same protocol for publishing
BM_PIN_COUNT_WAITER, rechecking the refcount, and returning false when
only our own pin remains.
---
src/backend/storage/buffer/bufmgr.c | 28 ++++------------------------
1 file changed, 4 insertions(+), 24 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 686265c3d48..277a1b4d993 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6844,34 +6844,14 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
-
- /*
- * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
- * The shared refcount can be decremented while BM_LOCKED is set, so
- * use an atomic operation that preserves concurrent refcount changes.
- */
- pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
-
/*
- * Recheck the refcount after publishing the waiter flag, while shared
- * refcount increments are still prevented by BM_LOCKED. If only our
- * pin remains, the cleanup-lock condition has already been satisfied,
- * so remove the waiter state and return without sleeping.
+ * Register ourselves as the pincount waiter. If the shared refcount
+ * was concurrently reduced to 1 (only our own pin remains),
+ * RegisterPinCountWaiter() returns false and no wait is necessary.
*/
- buf_state = pg_atomic_read_u64(&bufHdr->state);
-
- if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
- {
- UnlockBufHdrExt(bufHdr, buf_state,
- 0, BM_PIN_COUNT_WAITER,
- 0);
- PinCountWaitBuf = NULL;
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
goto cleanup_lock_acquired;
- }
- UnlockBufHdr(bufHdr);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-09-01 07:04 JoongHyuk Shin <sjh910805@gmail.com>
parent: Vitaly Davydov <vitprof@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: JoongHyuk Shin @ 2026-09-01 07:04 UTC (permalink / raw)
To: Vitaly Davydov <vitprof@gmail.com>; +Cc: Xuneng Zhou <xunengzhou@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
I've read v8, though I haven't tested it.
The recheck in RegisterPinCountWaiter() closes the window I described.
I also think 0002 is worth keeping.
--
JH Shin
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-09-01 12:52 Xuneng Zhou <xunengzhou@gmail.com>
parent: JoongHyuk Shin <sjh910805@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-09-01 12:52 UTC (permalink / raw)
To: JoongHyuk Shin <sjh910805@gmail.com>; Vitaly Davydov <vitprof@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>
Hi,
On Tue, Sep 1, 2026 at 3:05 PM JoongHyuk Shin <sjh910805@gmail.com> wrote:
>
> I've read v8, though I haven't tested it.
> The recheck in RegisterPinCountWaiter() closes the window I described.
> I also think 0002 is worth keeping.
Sorry for the late reply here.
* The caller must already be registered as the shared buffer's
+ * BM_PIN_COUNT_WAITER.
This line of comment for PinCountWaiterCheckReadyForCleanup in v8
seems not accurate to me. RegisterPinCountWaiter() explicitly permits
the shared bit BM_PIN_COUNT_WAITER to be absent. The invariante seems
to be:
Assert(PinCountWaitBuf == bufHdr); This process owns the logical
cleanup wait for this buffer.
Updated this, and extended the commit message like we discussed
earlier though it might not get used at the end. The timeout para in
that message is basically a summary of the first email in this thread.
I am not that familiar with it and too tired to proof-read it. Can you
do me a favor?
Beside these, I have no further comments on the patches. They seem to
be in good shape for a bug fix. Just wondering whether we need a test
for it.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
Attachments: download all (2 files, .tar.gz)
v9-0002-Use-RegisterPinCountWaiter-in-LockBufferForCleanu.patch (application/octet-stream, 2.3K) download
From da12409874b985042d78d35bd9dea31a2c03ae95 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:32:32 +0800
Subject: [PATCH v9 2/2] Use RegisterPinCountWaiter() in LockBufferForCleanup()
Replace the duplicated pincount-waiter registration logic in
LockBufferForCleanup() with a call to RegisterPinCountWaiter(), which
already encapsulates the same protocol for publishing
BM_PIN_COUNT_WAITER, rechecking the refcount, and returning false when
only our own pin remains.
---
src/backend/storage/buffer/bufmgr.c | 28 ++++------------------------
1 file changed, 4 insertions(+), 24 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index c96ceb1513a..a2c922620ae 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6842,34 +6842,14 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
-
- /*
- * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
- * The shared refcount can be decremented while BM_LOCKED is set, so
- * use an atomic operation that preserves concurrent refcount changes.
- */
- pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
-
/*
- * Recheck the refcount after publishing the waiter flag, while shared
- * refcount increments are still prevented by BM_LOCKED. If only our
- * pin remains, the cleanup-lock condition has already been satisfied,
- * so remove the waiter state and return without sleeping.
+ * Register ourselves as the pincount waiter. If the shared refcount
+ * was concurrently reduced to 1 (only our own pin remains),
+ * RegisterPinCountWaiter() returns false and no wait is necessary.
*/
- buf_state = pg_atomic_read_u64(&bufHdr->state);
-
- if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
- {
- UnlockBufHdrExt(bufHdr, buf_state,
- 0, BM_PIN_COUNT_WAITER,
- 0);
- PinCountWaitBuf = NULL;
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
goto cleanup_lock_acquired;
- }
- UnlockBufHdr(bufHdr);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
--
2.51.0
v9-0001-Fix-premature-wakeups-in-hot-standby-buffer-pin-c.patch (application/octet-stream, 10.9K) download
From c80a9bb1787d9efc4eb90fb8c0ce0e4bb105e230 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:31:08 +0800
Subject: [PATCH v9 1/2] Fix premature wakeups in hot-standby buffer-pin
conflict waits
ProcWaitForSignal() waits on the generic process latch and can therefore
return for reasons unrelated to either a buffer unpin or a
recovery-conflict timeout. ResolveRecoveryConflictWithBufferPin()
nevertheless treated every return as the end of the current wait,
disabled all timeouts, and returned to LockBufferForCleanup(), which
then armed a new relative deadlock timeout.
This can prevent the deadlock timeout from ever expiring. The timeout
subsystem may leave a kernel alarm armed after disabling the logical
timeout for which it was set, such as the startup-progress timeout. If
that alarm fires before the current buffer-pin deadlock deadline,
handle_sig_alarm() wakes the startup process even though no logical
timeout has expired, and reprograms the kernel alarm for the current
deadlock deadline. The resulting latch wake causes the wait code to
discard that deadline and arm a new relative deadline farther in the
future. The already-programmed kernel alarm now precedes the
replacement deadline and can wake the startup process prematurely
again. Repeating this sequence can postpone the deadlock timeout
unboundedly while the pin conflict persists.
Consequently, the startup process may never send the
RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK request. With no finite
max_standby_*_delay to provide another breaker, recovery can remain
stalled unboundedly.
After a wakeup, recheck whether only the startup process's own pin
remains. If other pins remain and neither recovery-conflict deadline
has been reached, ensure that the process remains registered as the
pin-count waiter and continue waiting without tearing down or
restarting the active timeouts. This makes unrelated latch wakeups
harmless while preserving the original timeout deadlines.
---
src/backend/storage/buffer/bufmgr.c | 104 ++++++++++++++++++++++++++++
src/backend/storage/ipc/standby.c | 84 +++++++++++++++-------
src/include/storage/bufmgr.h | 1 +
3 files changed, 163 insertions(+), 26 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 17f142e4c5b..c96ceb1513a 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3469,6 +3469,54 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process as the pincount waiter for a shared buffer
+ * and unlock the buffer header. Return true if the process was registered as
+ * the pincount waiter and must now wait to be signaled, or false if the
+ * shared refcount was concurrently reduced to 1 (only our own pin remains),
+ * in which case no wait is necessary.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other backend is already
+ * registered as the waiter.
+ */
+static bool
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+
+ /*
+ * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
+ * The shared refcount can be decremented while BM_LOCKED is set, so
+ * use an atomic operation that preserves concurrent refcount changes.
+ */
+ pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
+
+ /*
+ * Recheck the refcount after publishing the waiter flag, while shared
+ * refcount increments are still prevented by BM_LOCKED. If only our
+ * pin remains, the cleanup-lock condition has already been satisfied,
+ * so remove the waiter state and return without sleeping.
+ */
+ buf_state = pg_atomic_read_u64(&bufHdr->state);
+ if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
+ {
+ UnlockBufHdrExt(bufHdr, buf_state,
+ 0, BM_PIN_COUNT_WAITER,
+ 0);
+ PinCountWaitBuf = NULL;
+ return false;
+ }
+
+ UnlockBufHdr(bufHdr);
+
+ return true;
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -4763,6 +4811,62 @@ BufferGetLSNAtomic(Buffer buffer)
#endif
}
+/*
+ * PinCountWaiterCheckReadyForCleanup
+ * Recheck whether the current cleanup-lock wait has completed and, if
+ * necessary, rearm the shared pin-count notification.
+ *
+ * The caller must own the backend-local cleanup wait for this buffer, as
+ * indicated by PinCountWaitBuf. BM_PIN_COUNT_WAITER may still be set for
+ * this process, or it may already have been cleared by WakePinCountWaiter()
+ * before signaling us.
+ *
+ * Return true if our own pin is the only remaining shared pin. Otherwise,
+ * ensure that this process is registered as the shared pin-count waiter and
+ * return false.
+ */
+bool
+PinCountWaiterCheckReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple processes attempting to wait for pincount 1");
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again. The refcount may be concurrently
+ * reduced to 1 despite our holding the buffer header lock, in which case
+ * RegisterPinCountWaiter() returns false and the buffer is ready for
+ * cleanup.
+ */
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
+ return true;
+
+ return false;
+}
+
/* ---------------------------------------------------------------------
* DropRelationBuffers
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..044284b7ba5 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -790,22 +790,36 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBufId().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ int bufid;
Assert(InHotStandby);
+ bufid = GetStartupBufferPinWaitBufId();
+ Assert(bufid >= 0);
+
ltime = GetStandbyLimitTime();
- if (GetCurrentTimestamp() >= ltime && ltime != 0)
+ if (ltime != 0 && GetCurrentTimestamp() >= ltime)
{
/*
* We're already behind, so clear a path as quickly as possible.
*/
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+
+ /*
+ * Set delay timeout flag once the timeout is reached (the current
+ * timestamp is greater than the standby limit time). This variable is
+ * use by timeout handlers with the same purpose.
+ */
+ got_standby_delay_timeout = true;
}
else
{
@@ -833,35 +847,53 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
}
- /*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
- */
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
/*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
+ * Wait to be signaled by UnpinBuffer() or for the wait to be
+ * interrupted by one of the timeouts established above.
*
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
+ * ProcWaitForSignal() can also wake up for unrelated reasons, so
+ * recheck later whether cleanup can proceed.
*/
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ /*
+ * Once the reference count is 1, the waiter process itself is the
+ * only backend pinning the buffer at the moment. There is a chance to
+ * lock the buffer exclusively.
+ */
+ if (PinCountWaiterCheckReadyForCleanup(bufid + 1))
+ break;
+
+ /*
+ * Send the recovery conflict if the standby delay timeout is activated or
+ * standby limit time was already reached. The second condition handles the
+ * fast path when timeouts are not activated.
+ */
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..cf8ef54aedc 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -313,6 +313,7 @@ extern bool BufferIsPermanent(Buffer buffer);
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
ForkNumber *forknum, BlockNumber *blknum);
+extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std);
--
2.51.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-09-04 14:15 Vitaly Davydov <vitprof@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Vitaly Davydov @ 2026-09-04 14:15 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
Hi Xuneng Zhou,
> * The caller must already be registered as the shared buffer's
> + * BM_PIN_COUNT_WAITER.
> This line of comment for PinCountWaiterCheckReadyForCleanup in v8
> seems not accurate to me. RegisterPinCountWaiter() explicitly permits
> the shared bit BM_PIN_COUNT_WAITER to be absent.
Agreed, thank you. Your edits to the comment for
PinCountWaiterCheckReadyForCleanup() look great.
> The invariante seems to be: Assert(PinCountWaitBuf == bufHdr);
> This process owns the logical cleanup wait for this buffer.
Yes, this invariant is checked in PinCountWaiterCheckReadyForCleanup().
> Updated this, and extended the commit message like we discussed
> earlier though it might not get used at the end.
Thank you very much for such an excellent commit message. Prior to it
we discussed to unite the explanation in one single paragraph. It seems,
I did poorly and I missed the description of changes. Your commit looks
very descriptive and complete.
One note about the commit message is the combining of words with a hyphen.
I see, that hot-standby appears in commit messages, but the hyphen in
buffer-pin, recovery-conflict or startup-progress seems excessive.
I'm not sure, but the commit title ("Fix premature wakeups...") might
suggest
that the wakeups are being prevented entirely, rather than fixing how they
are handled.
> The timeout para in that message is basically a summary of the first
email
> in this thread. I am not that familiar with it and too tired to
proof-read it.
> Can you do me a favor?
The paragraph looks technically sound and explains the core issue in detail.
Previously, a hang could occur after just a single premature wake-up.
This is
now stated clearly. While interference from multiple active timeouts can
lead
to more complex behavior, the fundamental problem remains unchanged.
> Just wondering whether we need a test for it.
I have a test for issue reproduction that is attached to the first email and
the patch in [1] where test/recovery/t/031_recovery_conflict.pl was
updated to reproduce the issue. But it uses log_startup_progress_interval
to activate a premature wakeup. There is an opinion in [2] that
log_startup_progress_interval behaviour may be changed, that invalidates
the test. I have no ideas about other simple alternative ways to trigger
premature wakeups. May be, injection points may help.
There is a concern in [2] that changes to the behavior of
log_startup_progress_interval will invalidate the test. I cannot think of
other simple ways to trigger premature wakeups. Perhaps injection points
could help here.
[1]
https://www.postgresql.org/message-id/CAHGQGwFED52rRVb_a%3DbVSdbOA%3DoARVOUyw381GC_6uKoNWJAtQ%40mail...
[2]
https://www.postgresql.org/message-id/CAHGQGwHh5j%3DsG3AZh%3DHyuXtPM6cmymp6MJkRAMJcJUyN2qpHrQ%40mail...
With best regards,
Vitaly Davydov
vitprof@gmail.com (aka v.davydov@postgrespro.ru)
vitcpp@mail.ru
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-02 03:00 Xuneng Zhou <xunengzhou@gmail.com>
parent: Fujii Masao <masao.fujii@gmail.com>
1 sibling, 0 replies; 43+ messages in thread
From: Xuneng Zhou @ 2026-10-02 03:00 UTC (permalink / raw)
To: Fujii Masao <masao.fujii@gmail.com>; +Cc: Vitaly Davydov <v.davydov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Jun 5, 2026 at 7:58 PM Fujii Masao <masao.fujii@gmail.com> wrote:
>
> On Wed, May 27, 2026 at 12:01 AM Fujii Masao <masao.fujii@gmail.com> wrote:
> > + if (got_standby_delay_timeout)
> > + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
> > + else if (got_standby_deadlock_timeout)
> > + {
> >
> > Shouldn't we break out of the loop when either got_standby_delay_timeout or
> > got_standby_deadlock_timeout becomes true? Otherwise, the loop continues with
> > those flags still set, which could cause SendRecoveryConflictWithBufferPin() to
> > be called unnecessarily in the subsequent cycles.
> >
> >
> > + if (BufferGetRefCount(buffer) <= 1)
> >
> > Should this be "BufferGetRefCount(buffer) == 1" instead? I don't think
> > BufferGetRefCount(buffer) should ever return 0 here. If that's correct,
> > would it make sense to explicitly detect that case, for example:
> >
> > -----------------
> > uint32 refcount = BufferGetRefCount(buffer);
> >
> > Assert(refcount > 0);
> >
> > if (refcount == 0)
> > elog(ERROR, "buffer refcount dropped to zero while waiting for
> > cleanup lock");
> >
> > if (refcount == 1)
> > break;
> > -----------------
>
> I've updated the patch based on these comments.
> Attached is the latest version.
>
> I removed the TAP test from this patch for now. I'll consider adding
> a test for this separately later.
>
> BTW, I'm just wondering whether ResolveRecoveryConflictWithLock()
> might have the same issue. I need to investigate that further.
While working on something closely related, I revisited this thread.
ResolveRecoveryConflictWithLock seems to also suffer from the same
issue per investigation. I'll share some findings later.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-10 09:24 Xuneng Zhou <xunengzhou@gmail.com>
parent: Vitaly Davydov <vitprof@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-10-10 09:24 UTC (permalink / raw)
To: Vitaly Davydov <vitprof@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
Hi Vitaly,
On Fri, Sep 4, 2026 at 10:16 PM Vitaly Davydov <vitprof@gmail.com> wrote:
>
> Hi Xuneng Zhou,
>
> > * The caller must already be registered as the shared buffer's
> > + * BM_PIN_COUNT_WAITER.
> > This line of comment for PinCountWaiterCheckReadyForCleanup in v8
> > seems not accurate to me. RegisterPinCountWaiter() explicitly permits
> > the shared bit BM_PIN_COUNT_WAITER to be absent.
>
> Agreed, thank you. Your edits to the comment for
> PinCountWaiterCheckReadyForCleanup() look great.
>
> > The invariante seems to be: Assert(PinCountWaitBuf == bufHdr);
> > This process owns the logical cleanup wait for this buffer.
>
> Yes, this invariant is checked in PinCountWaiterCheckReadyForCleanup().
>
> > Updated this, and extended the commit message like we discussed
> > earlier though it might not get used at the end.
>
> Thank you very much for such an excellent commit message. Prior to it
> we discussed to unite the explanation in one single paragraph. It seems,
> I did poorly and I missed the description of changes. Your commit looks
> very descriptive and complete.
Thanks for proof-reading it!
> One note about the commit message is the combining of words with a hyphen.
> I see, that hot-standby appears in commit messages, but the hyphen in
> buffer-pin, recovery-conflict or startup-progress seems excessive.
>
> I'm not sure, but the commit title ("Fix premature wakeups...") might
> suggest
> that the wakeups are being prevented entirely, rather than fixing how they
> are handled.
Ok, that's right. Agreed that the header line is somewhat misleading
and needs to be softened. We did not *prevent* the general unrelated
premature wakeups in ProcWaitForSignal. And those hyphens are
unnecessary, they are removed. I also made some minor adjustments to
the patch. Add timeouts_armed flag to tell the already expired timeout
to avoid the extra round of cancellation request without
re-introducing the original problem. If it is set, break the for loop,
let the resolver send another cancellation request if the conflict is
still there.
There's another counterpart case for ResolveRecoveryConflictWithLock
as the attached reproducer 913 shows. The startup process waits for a
lock held by a standby session, which in turn waits for another lock
held by the startup process. An unrelated wakeup before the set
deadlock timeout causes the same kernel timeout rearming issue,
preventing the startup process from detecting and breaking the cycle.
> > The timeout para in that message is basically a summary of the first
> email
> > in this thread. I am not that familiar with it and too tired to
> proof-read it.
> > Can you do me a favor?
>
> The paragraph looks technically sound and explains the core issue in detail.
> Previously, a hang could occur after just a single premature wake-up.
> This is
> now stated clearly. While interference from multiple active timeouts can
> lead
> to more complex behavior, the fundamental problem remains unchanged.
> > Just wondering whether we need a test for it.
>
> I have a test for issue reproduction that is attached to the first email and
> the patch in [1] where test/recovery/t/031_recovery_conflict.pl was
> updated to reproduce the issue. But it uses log_startup_progress_interval
> to activate a premature wakeup. There is an opinion in [2] that
> log_startup_progress_interval behaviour may be changed, that invalidates
> the test. I have no ideas about other simple alternative ways to trigger
> premature wakeups. May be, injection points may help.
>
> There is a concern in [2] that changes to the behavior of
> log_startup_progress_interval will invalidate the test. I cannot think of
> other simple ways to trigger premature wakeups. Perhaps injection points
> could help here.
>
> [1]
> https://www.postgresql.org/message-id/CAHGQGwFED52rRVb_a%3DbVSdbOA%3DoARVOUyw381GC_6uKoNWJAtQ%40mail...
> [2]
> https://www.postgresql.org/message-id/CAHGQGwHh5j%3DsG3AZh%3DHyuXtPM6cmymp6MJkRAMJcJUyN2qpHrQ%40mail...
pg_log_backend_memory_contexts seems able to trigger the premature
wakeups for the startup. V10 adds two tests(lock/buffer pin) based on
that. As usual, the hard part is to make it deterministic end-to-end.
To make the stall occur, we need this sequence:
1) Startup sets a deadlock deadline and a kernel alarm for it.
2) An unrelated wakeup makes broken code move the deadline later,
leaving the old alarm pending.
3) If startup handles that alarm before the new deadline, the handler
wakes startup and schedules another alarm without firing the deadlock
timeout.
4) Broken code moves the deadline later again, leaving that alarm pending.
5) Steps 3–4 can repeat indefinitely.
The tricky part is in step 3. If there's some delay for the startup
handling the alarm due to scheduling or other factors, the woken
process would possibly find that the logical deadline has expired,
subsequently, the handler would fire the deadlock timeout to send the
cancellation request to break the cycle. There's no facility in the
tree to simulate the time to let it 'pause' until the startup handles
the alarm. Therefore, the added test exercises the mechanism rather
than the behavior -- the timeout would not be rearmed after unrelated
wakeups.
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
Attachments: download all (3 files, .tar.gz)
v10-0002-Test-recovery-conflict-waits-after-unrelated-wak.patch (application/octet-stream, 8.8K) download
From cac85405c5ff1af70bed78f67b82245f8217f0ae Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:31:08 +0800
Subject: [PATCH v10 2/3] Test recovery conflict waits after unrelated wakeups
Wake startup with pg_log_backend_memory_contexts() while a standby
session holds a conflicting buffer pin or relation lock. Attach a notice
injection point after the initial timer setup and check that startup
does not reach it again when it resumes waiting.
Set deadlock_timeout to one day so normal expiration does not interfere
with the check. Wait for startup's memory-context log message and then
its conflict wait before checking for a notice.
Also add a relation lock deadlock test. Let the session finish its first
deadlock check before completing the cycle, so startup must request
another check for recovery to continue.
---
src/backend/storage/ipc/standby.c | 2 +
src/test/recovery/t/031_recovery_conflict.pl | 143 ++++++++++++++++++-
2 files changed, 140 insertions(+), 5 deletions(-)
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 89319db0311..6438bca471f 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -699,6 +699,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
enable_timeouts(timeouts, cnt);
timeouts_armed = true;
+ INJECTION_POINT("standby-conflict-timeouts-armed", "lock");
}
/*
@@ -860,6 +861,7 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
timeouts_armed = true;
+ INJECTION_POINT("standby-conflict-timeouts-armed", "bufferpin");
}
/*
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index d3f4f591661..37a830f0ef9 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -223,6 +223,7 @@ $node_standby->adjust_conf(
'postgresql.conf',
'max_standby_streaming_delay',
"${PostgreSQL::Test::Utils::timeout_default}s");
+
$node_standby->restart();
$psql_standby->reconnect_and_clear();
@@ -247,7 +248,7 @@ SELECT txid_current();
$node_primary->wait_for_replay_catchup($node_standby);
-$res = $psql_standby->query_until(
+$psql_standby->query_until(
qr/^1$/m, qq[
BEGIN;
-- hold pin
@@ -256,9 +257,6 @@ $res = $psql_standby->query_until(
-- wait for lock held by prepared transaction
SELECT * FROM $table2;
]);
-ok(1,
- "$sect: cursor holding conflicting pin, also waiting for lock, established"
-);
# just to make sure we're waiting for lock already
ok( $node_standby->poll_query_until(
@@ -276,10 +274,55 @@ check_conflict_log("User transaction caused buffer deadlock with recovery.");
$psql_standby->reconnect_and_clear();
check_conflict_stat("deadlock");
+
+## RECOVERY CONFLICT 6: Lock deadlock
+$sect = "startup lock deadlock";
+
+# Restart to clear any alarm left by the buffer pin test. Disable progress
+# logging so it does not set another timer.
+$node_standby->append_conf('postgresql.conf',
+ 'log_startup_progress_interval = 0');
+$node_standby->restart();
+$psql_standby->reconnect_and_clear();
+
+# Hold a lock on table1 while waiting for startup's lock on table2. Let the
+# session finish its first deadlock check before making startup wait for
+# table1. That first check finds no deadlock, so startup must ask the
+# session to check again once both processes are waiting for each other.
+my $standby_pid = $psql_standby->query_safe('SELECT pg_backend_pid()');
+$log_location = -s $node_standby->logfile;
+$psql_standby->query_until(
+ qr/^1$/m, qq[
+ BEGIN;
+ SET LOCAL deadlock_timeout = '10ms';
+ SET LOCAL log_lock_waits = on;
+ LOCK TABLE $table1 IN ACCESS SHARE MODE;
+ SELECT 1;
+ SELECT * FROM $table2;
+]);
+$node_standby->wait_for_log(
+ qr/process $standby_pid still waiting for AccessShareLock/,
+ $log_location);
+
+# Make startup wait for the session's lock on table1, creating the deadlock.
+$node_primary->safe_psql($test_db, qq[BEGIN; LOCK TABLE $table1; COMMIT;]);
+$node_primary->wait_for_replay_catchup($node_standby);
+
+# Check that the session reports a deadlock and increments the deadlock count.
+$node_standby->wait_for_log(qr/ERROR: deadlock detected/, $log_location);
+$psql_standby->reconnect_and_clear();
+ok( $node_standby->poll_query_until(
+ $test_db,
+ qq[SELECT deadlocks FROM pg_stat_database WHERE datname='$test_db';],
+ '1'),
+ "$sect: stats show lock deadlock on standby");
+
# clean up for next tests
$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
'50ms');
+$node_standby->adjust_conf('postgresql.conf', 'log_startup_progress_interval',
+ undef);
$node_standby->restart();
$psql_standby->reconnect_and_clear();
@@ -297,7 +340,7 @@ cmp_ok( $node_standby->safe_psql(
);
-## RECOVERY CONFLICT 6: Database conflict
+## RECOVERY CONFLICT 7: Database conflict
$sect = "database conflict";
$node_primary->safe_psql('postgres', qq[DROP DATABASE $test_db;]);
@@ -311,6 +354,96 @@ check_conflict_log("User was connected to a database that must be dropped");
# windows
$psql_standby->quit;
+# An unrelated wakeup must not restart the deadlock timer in either recovery
+# wait. Run these tests last because they use a long timeout.
+if ( $ENV{enable_injection_points} eq 'yes'
+ && $node_primary->check_extension('injection_points'))
+{
+ $node_primary->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ $node_standby->adjust_conf('postgresql.conf',
+ 'max_standby_streaming_delay', '-1');
+ $node_standby->append_conf('postgresql.conf',
+ 'log_startup_progress_interval = 0');
+ # Set the deadlock timeout far beyond the expected test duration.
+ $node_standby->adjust_conf('postgresql.conf', 'deadlock_timeout', '1d');
+
+ # Log startup's notices so we can see if it sets the timer again.
+ $node_standby->append_conf('postgresql.conf',
+ q[log_min_messages = 'warning, startup:notice']);
+ $node_standby->restart();
+
+ my $startup_pid = $node_standby->safe_psql('postgres',
+ q[SELECT pid FROM pg_stat_activity WHERE backend_type = 'startup']);
+ my $point = 'standby-conflict-timeouts-armed';
+
+ for my $kind ('bufferpin', 'lock')
+ {
+ my $table = "test_recovery_wakeup_$kind";
+ my $wait_event = $kind eq 'bufferpin' ? 'BufferCleanup' : 'relation';
+
+ # Leave aborted rows for VACUUM to remove. Commit another insert to
+ # flush the WAL, then replay it before taking the cursor's snapshot.
+ $node_primary->safe_psql(
+ 'postgres', qq[
+CREATE TABLE $table(a int) WITH (autovacuum_enabled = false);
+INSERT INTO $table VALUES (1);
+BEGIN;
+INSERT INTO $table SELECT generate_series(1, 100);
+ROLLBACK;
+INSERT INTO $table VALUES (2);
+]);
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ my $session = $node_standby->background_psql('postgres');
+ $session->query_safe(
+ $kind eq 'bufferpin'
+ ? "BEGIN; DECLARE c CURSOR FOR SELECT a FROM $table; FETCH FROM c;"
+ : "BEGIN; LOCK TABLE $table IN ACCESS SHARE MODE;");
+
+ $node_primary->safe_psql('postgres',
+ $kind eq 'bufferpin'
+ ? "VACUUM FREEZE $table"
+ : "BEGIN; LOCK TABLE $table; COMMIT;");
+ $node_standby->wait_for_event('startup', $wait_event);
+
+ # The timer is already set. Log any attempt by this resolver to set it
+ # again.
+ $node_standby->safe_psql('postgres',
+ "SELECT injection_points_attach('$point', 'notice', '$kind');");
+
+ # Wake startup while the session still holds the pin or lock. Wait for
+ # its log message so we know it has left the original wait.
+ my $log_offset = -s $node_standby->logfile;
+ is( $node_standby->safe_psql(
+ 'postgres',
+ "SELECT pg_log_backend_memory_contexts($startup_pid)"),
+ 't',
+ "$kind: memory-context request sent to startup");
+ $node_standby->wait_for_log(
+ qr/logging memory contexts of PID $startup_pid\b/, $log_offset);
+
+ # Wait for startup to resume its wait for the pin or lock. Startup
+ # writes notices directly to the test log, so any notice from setting
+ # the timer again must be in the file by then.
+ $node_standby->wait_for_event('startup', $wait_event);
+ unlike(
+ slurp_file($node_standby->logfile, $log_offset),
+ qr/notice triggered for injection point \Q$point\E/,
+ "$kind: unrelated wakeup did not restart the deadlock timer");
+
+ # Stop recording timer setup before releasing the pin or lock.
+ $node_standby->safe_psql('postgres',
+ "SELECT injection_points_detach('$point');");
+ $session->query_safe('COMMIT;');
+ # Wait for replay too: VACUUM can return before flushing its WAL.
+ $node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+ $session->quit;
+ }
+}
+
$node_standby->stop();
$node_primary->stop();
--
2.51.0
v10-0003-Use-RegisterPinCountWaiter-in-LockBufferForClean.patch (application/octet-stream, 2.3K) download
From 4d2cf56d5ad7b279ba07dca049987613068b2010 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:32:32 +0800
Subject: [PATCH v10 3/3] Use RegisterPinCountWaiter() in
LockBufferForCleanup()
Replace the duplicated pincount-waiter registration logic in
LockBufferForCleanup() with a call to RegisterPinCountWaiter(), which
already encapsulates the same protocol for publishing
BM_PIN_COUNT_WAITER, rechecking the refcount, and returning false when
only our own pin remains.
---
src/backend/storage/buffer/bufmgr.c | 28 ++++------------------------
1 file changed, 4 insertions(+), 24 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6733c3c2dcd..09189de491c 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6846,34 +6846,14 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
-
- /*
- * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
- * The shared refcount can be decremented while BM_LOCKED is set, so
- * use an atomic operation that preserves concurrent refcount changes.
- */
- pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
-
/*
- * Recheck the refcount after publishing the waiter flag, while shared
- * refcount increments are still prevented by BM_LOCKED. If only our
- * pin remains, the cleanup-lock condition has already been satisfied,
- * so remove the waiter state and return without sleeping.
+ * Register ourselves as the pincount waiter. If the shared refcount
+ * was concurrently reduced to 1 (only our own pin remains),
+ * RegisterPinCountWaiter() returns false and no wait is necessary.
*/
- buf_state = pg_atomic_read_u64(&bufHdr->state);
-
- if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
- {
- UnlockBufHdrExt(bufHdr, buf_state,
- 0, BM_PIN_COUNT_WAITER,
- 0);
- PinCountWaitBuf = NULL;
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
goto cleanup_lock_acquired;
- }
- UnlockBufHdr(bufHdr);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
--
2.51.0
v10-0001-Handle-unrelated-wakeups-during-recovery-buffer-.patch (application/octet-stream, 14.4K) download
From e9593403d2624867e89c5906a8a03bd8e9635cd8 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:31:08 +0800
Subject: [PATCH v10 1/3] Handle unrelated wakeups during recovery buffer pin
and lock waits
ProcWaitForSignal() can wake the startup process even when the buffer
pin or lock it is waiting for has not been released and no timeout has
expired. Both recovery conflict handlers would then cancel their timers
and return. If the conflict remained, the next call would start a new
deadlock timeout with a later deadline.
An alarm left over from an earlier wait can make this happen repeatedly.
Canceling a logical timeout does not necessarily cancel the kernel
alarm. If that alarm fires before the current deadlock deadline, the
signal handler wakes startup and schedules another alarm for that
deadline. Startup then restarts its deadlock timeout, moving the deadline
past the pending alarm again. This can continue indefinitely, so the
deadlock timeout never fires.
Startup may therefore never ask the conflicting backends to check for
deadlocks. If the applicable max_standby_streaming_delay or
max_standby_archive_delay is -1, there is no delay limit to force
cancellation, and recovery can remain stuck.
Keep the existing timers running when a wakeup does not end the wait.
For buffer pins, check whether other processes still hold pins and,
if necessary, register for another notification when they release them.
For relation locks, check whether startup is still waiting for the lock.
If the conflict remains and neither timeout has expired, wait again
without restarting the timers.
When the standby delay limit has already passed and no timers were set,
continue returning after a wakeup so the caller can retry.
---
src/backend/storage/buffer/bufmgr.c | 102 ++++++++++++++++
src/backend/storage/ipc/standby.c | 178 +++++++++++++++++-----------
src/include/storage/bufmgr.h | 1 +
3 files changed, 213 insertions(+), 68 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f81c7732e68..6733c3c2dcd 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3471,6 +3471,52 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process to be notified when only its own pin remains,
+ * then unlock the buffer header. Return true if the process needs to wait,
+ * or false if the pin count has already dropped to one.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other process is already
+ * registered as the waiter.
+ */
+static bool
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+
+ /*
+ * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
+ * The shared refcount can be decremented while BM_LOCKED is set, so
+ * use an atomic operation that preserves concurrent refcount changes.
+ */
+ pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
+
+ /*
+ * Recheck the refcount after publishing the waiter flag, while shared
+ * refcount increments are still prevented by BM_LOCKED. If only our
+ * pin remains, the cleanup-lock condition has already been satisfied,
+ * so remove the waiter state and return without sleeping.
+ */
+ buf_state = pg_atomic_read_u64(&bufHdr->state);
+ if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
+ {
+ UnlockBufHdrExt(bufHdr, buf_state,
+ 0, BM_PIN_COUNT_WAITER,
+ 0);
+ PinCountWaitBuf = NULL;
+ return false;
+ }
+
+ UnlockBufHdr(bufHdr);
+
+ return true;
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -6679,6 +6725,62 @@ CheckBufferIsPinnedOnce(Buffer buffer)
}
}
+/*
+ * PinCountWaiterCheckReadyForCleanup
+ * Check whether only our own pin remains and, if necessary, register
+ * for another notification.
+ *
+ * The caller must own the backend-local cleanup wait for this buffer, as
+ * indicated by PinCountWaitBuf. BM_PIN_COUNT_WAITER may still be set for
+ * this process, or it may already have been cleared by WakePinCountWaiter()
+ * before signaling us.
+ *
+ * Return true if our own pin is the only remaining shared pin. Otherwise,
+ * ensure that this process is registered as the shared pin-count waiter and
+ * return false.
+ */
+bool
+PinCountWaiterCheckReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple backends attempting to wait for pincount 1");
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again. The refcount may be concurrently
+ * reduced to 1 despite our holding the buffer header lock, in which case
+ * RegisterPinCountWaiter() returns false and the buffer is ready for
+ * cleanup.
+ */
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
+ return true;
+
+ return false;
+}
+
/*
* LockBufferForCleanup - lock a buffer in preparation for deleting items
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..89319db0311 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -627,6 +627,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
{
TimestampTz ltime;
TimestampTz now;
+ bool timeouts_armed = false;
Assert(InHotStandby);
@@ -697,61 +698,80 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
cnt++;
enable_timeouts(timeouts, cnt);
+ timeouts_armed = true;
}
- /* Wait to be signaled by the release of the Relation Lock */
- ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
-
/*
- * Exit if ltime is reached. Then all the backends holding conflicting
- * locks will be canceled in the next ResolveRecoveryConflictWithLock()
- * call.
+ * ProcWaitForSignal() can wake up even when the lock wait has not ended
+ * and neither timeout has expired. Keep waiting with the same timeouts
+ * in that case. Returning to the caller would restart the deadlock
+ * timeout with a later deadline, which could keep it from ever expiring.
+ *
+ * If the standby delay limit was already reached above, no timeouts were
+ * set. Return after any wakeup so the caller can try resolving the
+ * conflict again.
*/
- if (got_standby_lock_timeout)
- goto cleanup;
-
- if (got_standby_deadlock_timeout)
+ for (;;)
{
- VirtualTransactionId *backends;
+ ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
- backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+ if (!timeouts_armed)
+ break;
- /* Quick exit if there's no work to be done */
- if (!VirtualTransactionIdIsValid(*backends))
- goto cleanup;
+ if (*((volatile ProcWaitStatus *) &MyProc->waitStatus) != PROC_WAIT_STATUS_WAITING)
+ break;
/*
- * Send signals to all the backends holding the conflicting locks, to
- * ask them to check themselves for deadlocks.
+ * Exit if ltime is reached. Then all the backends holding conflicting
+ * locks will be canceled in the next
+ * ResolveRecoveryConflictWithLock() call.
*/
- while (VirtualTransactionIdIsValid(*backends))
+ if (got_standby_lock_timeout)
+ break;
+
+ if (got_standby_deadlock_timeout)
{
- (void) SignalRecoveryConflictWithVirtualXID(*backends,
- RECOVERY_CONFLICT_STARTUP_DEADLOCK);
- backends++;
- }
+ VirtualTransactionId *backends;
- /*
- * Exit if the recovery conflict has not been logged yet even though
- * logging is enabled, so that the caller can log that. Then
- * RecoveryConflictWithLock() is called again and we will wait again
- * for the lock to be released.
- */
- if (logging_conflict)
- goto cleanup;
+ backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
- /*
- * Wait again here to be signaled by the release of the Relation Lock,
- * to prevent the subsequent RecoveryConflictWithLock() from causing
- * deadlock_timeout and sending a request for deadlocks check again.
- * Otherwise the request continues to be sent every deadlock_timeout
- * until the relation locks are released or ltime is reached.
- */
- got_standby_deadlock_timeout = false;
- ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
- }
+ /* Quick exit if there's no work to be done */
+ if (!VirtualTransactionIdIsValid(*backends))
+ break;
+
+ /*
+ * Send signals to all the backends holding the conflicting locks,
+ * to ask them to check themselves for deadlocks.
+ */
+ while (VirtualTransactionIdIsValid(*backends))
+ {
+ (void) SignalRecoveryConflictWithVirtualXID(*backends,
+ RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ backends++;
+ }
-cleanup:
+ /*
+ * Exit if the recovery conflict has not been logged yet even
+ * though logging is enabled, so that the caller can log that.
+ * Then RecoveryConflictWithLock() is called again and we will
+ * wait again for the lock to be released.
+ */
+ if (logging_conflict)
+ break;
+
+ /*
+ * Wait again here to be signaled by the release of the Relation
+ * Lock, to prevent the subsequent RecoveryConflictWithLock() from
+ * causing deadlock_timeout and sending a request for deadlocks
+ * check again. Otherwise the request continues to be sent every
+ * deadlock_timeout until the relation locks are released or ltime
+ * is reached.
+ */
+ got_standby_deadlock_timeout = false;
+ ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
+ break;
+ }
+ }
/*
* Clear any timeout requests established above. We assume here that the
@@ -790,17 +810,25 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBuf().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ Buffer buffer;
+ bool timeouts_armed = false;
Assert(InHotStandby);
+ buffer = GetStartupBufferPinWaitBuf();
+ Assert(BufferIsValid(buffer));
+
ltime = GetStandbyLimitTime();
- if (GetCurrentTimestamp() >= ltime && ltime != 0)
+ if (ltime != 0 && GetCurrentTimestamp() >= ltime)
{
/*
* We're already behind, so clear a path as quickly as possible.
@@ -831,37 +859,51 @@ ResolveRecoveryConflictWithBufferPin(void)
cnt++;
enable_timeouts(timeouts, cnt);
+ timeouts_armed = true;
}
/*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
+ * ProcWaitForSignal() can wake up for unrelated reasons. If timeouts
+ * were set, keep waiting with the same deadlines until a timeout expires
+ * or only our own pin remains. The helper also makes sure we are
+ * registered for another unpin notification before waiting again. If the
+ * delay limit had already passed, return after any wakeup so the caller
+ * can retry.
*/
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
- /*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
- *
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
- */
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ if (!timeouts_armed)
+ break;
+
+ if (PinCountWaiterCheckReadyForCleanup(buffer))
+ break;
+
+ /* Request cancellation if the delay timeout has expired. */
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..a3d297fb2ee 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -341,6 +341,7 @@ LockBuffer(Buffer buffer, BufferLockMode mode)
extern bool ConditionalLockBuffer(Buffer buffer);
extern void LockBufferForCleanup(Buffer buffer);
+extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
extern bool ConditionalLockBufferForCleanup(Buffer buffer);
extern bool IsBufferCleanupOK(Buffer buffer);
extern bool HoldingBufferPinThatDelaysRecovery(void);
--
2.51.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-10 09:27 Xuneng Zhou <xunengzhou@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-10-10 09:27 UTC (permalink / raw)
To: Vitaly Davydov <vitprof@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
On Sat, Oct 10, 2026 at 5:24 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
>
> Hi Vitaly,
>
> On Fri, Sep 4, 2026 at 10:16 PM Vitaly Davydov <vitprof@gmail.com> wrote:
> >
> > Hi Xuneng Zhou,
> >
> > > * The caller must already be registered as the shared buffer's
> > > + * BM_PIN_COUNT_WAITER.
> > > This line of comment for PinCountWaiterCheckReadyForCleanup in v8
> > > seems not accurate to me. RegisterPinCountWaiter() explicitly permits
> > > the shared bit BM_PIN_COUNT_WAITER to be absent.
> >
> > Agreed, thank you. Your edits to the comment for
> > PinCountWaiterCheckReadyForCleanup() look great.
> >
> > > The invariante seems to be: Assert(PinCountWaitBuf == bufHdr);
> > > This process owns the logical cleanup wait for this buffer.
> >
> > Yes, this invariant is checked in PinCountWaiterCheckReadyForCleanup().
> >
> > > Updated this, and extended the commit message like we discussed
> > > earlier though it might not get used at the end.
> >
> > Thank you very much for such an excellent commit message. Prior to it
> > we discussed to unite the explanation in one single paragraph. It seems,
> > I did poorly and I missed the description of changes. Your commit looks
> > very descriptive and complete.
>
> Thanks for proof-reading it!
>
> > One note about the commit message is the combining of words with a hyphen.
> > I see, that hot-standby appears in commit messages, but the hyphen in
> > buffer-pin, recovery-conflict or startup-progress seems excessive.
> >
> > I'm not sure, but the commit title ("Fix premature wakeups...") might
> > suggest
> > that the wakeups are being prevented entirely, rather than fixing how they
> > are handled.
>
> Ok, that's right. Agreed that the header line is somewhat misleading
> and needs to be softened. We did not *prevent* the general unrelated
> premature wakeups in ProcWaitForSignal. And those hyphens are
> unnecessary, they are removed. I also made some minor adjustments to
> the patch. Add timeouts_armed flag to tell the already expired timeout
> to avoid the extra round of cancellation request without
> re-introducing the original problem. If it is set, break the for loop,
> let the resolver send another cancellation request if the conflict is
> still there.
>
> There's another counterpart case for ResolveRecoveryConflictWithLock
> as the attached reproducer 913 shows. The startup process waits for a
> lock held by a standby session, which in turn waits for another lock
> held by the startup process. An unrelated wakeup before the set
> deadlock timeout causes the same kernel timeout rearming issue,
> preventing the startup process from detecting and breaking the cycle.
>
> > > The timeout para in that message is basically a summary of the first
> > email
> > > in this thread. I am not that familiar with it and too tired to
> > proof-read it.
> > > Can you do me a favor?
> >
> > The paragraph looks technically sound and explains the core issue in detail.
> > Previously, a hang could occur after just a single premature wake-up.
> > This is
> > now stated clearly. While interference from multiple active timeouts can
> > lead
> > to more complex behavior, the fundamental problem remains unchanged.
>
>
>
> > > Just wondering whether we need a test for it.
> >
> > I have a test for issue reproduction that is attached to the first email and
> > the patch in [1] where test/recovery/t/031_recovery_conflict.pl was
> > updated to reproduce the issue. But it uses log_startup_progress_interval
> > to activate a premature wakeup. There is an opinion in [2] that
> > log_startup_progress_interval behaviour may be changed, that invalidates
> > the test. I have no ideas about other simple alternative ways to trigger
> > premature wakeups. May be, injection points may help.
> >
> > There is a concern in [2] that changes to the behavior of
> > log_startup_progress_interval will invalidate the test. I cannot think of
> > other simple ways to trigger premature wakeups. Perhaps injection points
> > could help here.
> >
> > [1]
> > https://www.postgresql.org/message-id/CAHGQGwFED52rRVb_a%3DbVSdbOA%3DoARVOUyw381GC_6uKoNWJAtQ%40mail...
> > [2]
> > https://www.postgresql.org/message-id/CAHGQGwHh5j%3DsG3AZh%3DHyuXtPM6cmymp6MJkRAMJcJUyN2qpHrQ%40mail...
>
> pg_log_backend_memory_contexts seems able to trigger the premature
> wakeups for the startup. V10 adds two tests(lock/buffer pin) based on
> that. As usual, the hard part is to make it deterministic end-to-end.
>
> To make the stall occur, we need this sequence:
>
> 1) Startup sets a deadlock deadline and a kernel alarm for it.
> 2) An unrelated wakeup makes broken code move the deadline later,
> leaving the old alarm pending.
> 3) If startup handles that alarm before the new deadline, the handler
> wakes startup and schedules another alarm without firing the deadlock
> timeout.
> 4) Broken code moves the deadline later again, leaving that alarm pending.
> 5) Steps 3–4 can repeat indefinitely.
>
> The tricky part is in step 3. If there's some delay for the startup
> handling the alarm due to scheduling or other factors, the woken
> process would possibly find that the logical deadline has expired,
> subsequently, the handler would fire the deadlock timeout to send the
> cancellation request to break the cycle. There's no facility in the
> tree to simulate the time to let it 'pause' until the startup handles
> the alarm. Therefore, the added test exercises the mechanism rather
> than the behavior -- the timeout would not be rearmed after unrelated
> wakeups.
Sorry for the missing reproducer.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
Attachments:
913_standby_lock_deadlock_after_wakeup.pl (text/x-perl-script, 4.2K) download
# Reproducer: a hot standby deadlock between the startup process, waiting
# for a lock held by a session, and that session, waiting for a lock that the
# startup process holds, is never detected after an unrelated wakeup of the
# startup process. With max_standby_streaming_delay = -1, recovery is stuck.
#
# A standby session holds a lock on table1 and waits for a lock on table2
# that the startup process holds for a prepared transaction. Its own
# deadlock check runs before replay waits for table1, and finds nothing.
# Replay of LOCK TABLE table1 then makes the startup process wait for the
# session. Now only the startup process can find the deadlock: after
# deadlock_timeout, it asks the session to check again.
#
# Any wakeup that does not end the startup process's wait makes it cancel its
# timers and start a new deadlock timeout. The kernel alarm for the old one
# stays pending and wakes it again before the new one expires, and so on, so
# the request is never sent. Here a memory-context request provides the
# first wakeup.
#
# The assertions below describe the deadlock, so they pass on affected
# servers.
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my $primary = PostgreSQL::Test::Cluster->new('primary');
$primary->init(allows_streaming => 1);
$primary->append_conf('postgresql.conf', 'max_prepared_transactions = 10');
$primary->start;
$primary->backup('backup');
# Disable the startup progress timer, so that the memory-context request is
# the only wakeup.
my $standby = PostgreSQL::Test::Cluster->new('standby');
$standby->init_from_backup($primary, 'backup', has_streaming => 1);
$standby->append_conf(
'postgresql.conf', q[
max_standby_streaming_delay = -1
deadlock_timeout = 3s
log_recovery_conflict_waits = on
log_startup_progress_interval = 0
]);
$standby->start;
# Make the startup process hold an AccessExclusiveLock on table2.
$primary->safe_psql(
'postgres', q[
CREATE TABLE table1(a int);
CREATE TABLE table2(a int);
BEGIN;
LOCK TABLE table2;
PREPARE TRANSACTION 'lock';
]);
$primary->wait_for_replay_catchup($standby);
# The session locks table1, then waits for the lock on table2. Make its own
# deadlock check run quickly, and log that it ran, so that it finishes before
# the startup process waits for table1.
my $session = $standby->background_psql('postgres', on_error_stop => 0);
my $session_pid = $session->query_safe('SELECT pg_backend_pid()');
my $log_offset = -s $standby->logfile;
$session->query_until(
qr/^1$/m, q[
BEGIN;
SET LOCAL deadlock_timeout = '10ms';
SET LOCAL log_lock_waits = on;
LOCK TABLE table1 IN ACCESS SHARE MODE;
SELECT 1;
SELECT * FROM table2;
]);
$standby->wait_for_log(
qr/process $session_pid still waiting for AccessShareLock/, $log_offset);
# Replaying the lock on table1 makes the startup process wait for the
# session.
$primary->safe_psql('postgres', 'BEGIN; LOCK TABLE table1; COMMIT;');
$standby->wait_for_event('startup', 'relation');
ok(1, 'startup process waits for the session');
# Wake the startup process before its deadlock timeout expires.
my $startup_pid = $standby->safe_psql('postgres',
"SELECT pid FROM pg_stat_activity WHERE backend_type = 'startup'");
sleep(1);
$standby->safe_psql('postgres',
"SELECT pg_log_backend_memory_contexts($startup_pid)");
$standby->wait_for_log(qr/logging memory contexts of PID $startup_pid\b/,
$log_offset);
# deadlock_timeout passes several times, but the deadlock is not resolved.
sleep(10);
is( $standby->safe_psql(
'postgres',
"SELECT wait_event FROM pg_stat_activity WHERE backend_type = 'startup'"
),
'relation',
'startup process is still waiting for the session');
is( $standby->safe_psql(
'postgres',
"SELECT wait_event_type || ':' || wait_event FROM pg_stat_activity WHERE pid = $session_pid"
),
'Lock:relation',
'session is still waiting for the startup process');
unlike(
slurp_file($standby->logfile, $log_offset),
qr/ERROR: deadlock detected/,
'no deadlock was detected');
# Break the cycle by canceling the session.
$standby->safe_psql('postgres', "SELECT pg_cancel_backend($session_pid)");
$primary->wait_for_replay_catchup($standby);
ok(1, 'replay resumes once the session is canceled');
$session->quit;
$primary->safe_psql('postgres', q[ROLLBACK PREPARED 'lock']);
done_testing();
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-10 09:31 Xuneng Zhou <xunengzhou@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Xuneng Zhou @ 2026-10-10 09:31 UTC (permalink / raw)
To: Vitaly Davydov <vitprof@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
On Sat, Oct 10, 2026 at 5:27 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
>
> On Sat, Oct 10, 2026 at 5:24 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
> >
> > Hi Vitaly,
> >
> > On Fri, Sep 4, 2026 at 10:16 PM Vitaly Davydov <vitprof@gmail.com> wrote:
> > >
> > > Hi Xuneng Zhou,
> > >
> > > > * The caller must already be registered as the shared buffer's
> > > > + * BM_PIN_COUNT_WAITER.
> > > > This line of comment for PinCountWaiterCheckReadyForCleanup in v8
> > > > seems not accurate to me. RegisterPinCountWaiter() explicitly permits
> > > > the shared bit BM_PIN_COUNT_WAITER to be absent.
> > >
> > > Agreed, thank you. Your edits to the comment for
> > > PinCountWaiterCheckReadyForCleanup() look great.
> > >
> > > > The invariante seems to be: Assert(PinCountWaitBuf == bufHdr);
> > > > This process owns the logical cleanup wait for this buffer.
> > >
> > > Yes, this invariant is checked in PinCountWaiterCheckReadyForCleanup().
> > >
> > > > Updated this, and extended the commit message like we discussed
> > > > earlier though it might not get used at the end.
> > >
> > > Thank you very much for such an excellent commit message. Prior to it
> > > we discussed to unite the explanation in one single paragraph. It seems,
> > > I did poorly and I missed the description of changes. Your commit looks
> > > very descriptive and complete.
> >
> > Thanks for proof-reading it!
> >
> > > One note about the commit message is the combining of words with a hyphen.
> > > I see, that hot-standby appears in commit messages, but the hyphen in
> > > buffer-pin, recovery-conflict or startup-progress seems excessive.
> > >
> > > I'm not sure, but the commit title ("Fix premature wakeups...") might
> > > suggest
> > > that the wakeups are being prevented entirely, rather than fixing how they
> > > are handled.
> >
> > Ok, that's right. Agreed that the header line is somewhat misleading
> > and needs to be softened. We did not *prevent* the general unrelated
> > premature wakeups in ProcWaitForSignal. And those hyphens are
> > unnecessary, they are removed. I also made some minor adjustments to
> > the patch. Add timeouts_armed flag to tell the already expired timeout
> > to avoid the extra round of cancellation request without
> > re-introducing the original problem. If it is set, break the for loop,
> > let the resolver send another cancellation request if the conflict is
> > still there.
> >
> > There's another counterpart case for ResolveRecoveryConflictWithLock
> > as the attached reproducer 913 shows. The startup process waits for a
> > lock held by a standby session, which in turn waits for another lock
> > held by the startup process. An unrelated wakeup before the set
> > deadlock timeout causes the same kernel timeout rearming issue,
> > preventing the startup process from detecting and breaking the cycle.
> >
> > > > The timeout para in that message is basically a summary of the first
> > > email
> > > > in this thread. I am not that familiar with it and too tired to
> > > proof-read it.
> > > > Can you do me a favor?
> > >
> > > The paragraph looks technically sound and explains the core issue in detail.
> > > Previously, a hang could occur after just a single premature wake-up.
> > > This is
> > > now stated clearly. While interference from multiple active timeouts can
> > > lead
> > > to more complex behavior, the fundamental problem remains unchanged.
> >
> >
> >
> > > > Just wondering whether we need a test for it.
> > >
> > > I have a test for issue reproduction that is attached to the first email and
> > > the patch in [1] where test/recovery/t/031_recovery_conflict.pl was
> > > updated to reproduce the issue. But it uses log_startup_progress_interval
> > > to activate a premature wakeup. There is an opinion in [2] that
> > > log_startup_progress_interval behaviour may be changed, that invalidates
> > > the test. I have no ideas about other simple alternative ways to trigger
> > > premature wakeups. May be, injection points may help.
> > >
> > > There is a concern in [2] that changes to the behavior of
> > > log_startup_progress_interval will invalidate the test. I cannot think of
> > > other simple ways to trigger premature wakeups. Perhaps injection points
> > > could help here.
> > >
> > > [1]
> > > https://www.postgresql.org/message-id/CAHGQGwFED52rRVb_a%3DbVSdbOA%3DoARVOUyw381GC_6uKoNWJAtQ%40mail...
> > > [2]
> > > https://www.postgresql.org/message-id/CAHGQGwHh5j%3DsG3AZh%3DHyuXtPM6cmymp6MJkRAMJcJUyN2qpHrQ%40mail...
> >
> > pg_log_backend_memory_contexts seems able to trigger the premature
> > wakeups for the startup. V10 adds two tests(lock/buffer pin) based on
> > that. As usual, the hard part is to make it deterministic end-to-end.
> >
> > To make the stall occur, we need this sequence:
> >
> > 1) Startup sets a deadlock deadline and a kernel alarm for it.
> > 2) An unrelated wakeup makes broken code move the deadline later,
> > leaving the old alarm pending.
> > 3) If startup handles that alarm before the new deadline, the handler
> > wakes startup and schedules another alarm without firing the deadlock
> > timeout.
> > 4) Broken code moves the deadline later again, leaving that alarm pending.
> > 5) Steps 3–4 can repeat indefinitely.
> >
> > The tricky part is in step 3. If there's some delay for the startup
> > handling the alarm due to scheduling or other factors, the woken
> > process would possibly find that the logical deadline has expired,
> > subsequently, the handler would fire the deadlock timeout to send the
> > cancellation request to break the cycle. There's no facility in the
> > tree to simulate the time to let it 'pause' until the startup handles
> > the alarm. Therefore, the added test exercises the mechanism rather
> > than the behavior -- the timeout would not be rearmed after unrelated
> > wakeups.
>
> Sorry for the missing reproducer.
Add them to the commifest [1]. Attach again to let the cfbots do their work.
[1] https://commitfest.postgresql.org/patch/7372/
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
Attachments: download all (3 files, .tar.gz)
v10-0001-Handle-unrelated-wakeups-during-recovery-buffer-.patch (application/octet-stream, 14.4K) download
From e9593403d2624867e89c5906a8a03bd8e9635cd8 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:31:08 +0800
Subject: [PATCH v10 1/3] Handle unrelated wakeups during recovery buffer pin
and lock waits
ProcWaitForSignal() can wake the startup process even when the buffer
pin or lock it is waiting for has not been released and no timeout has
expired. Both recovery conflict handlers would then cancel their timers
and return. If the conflict remained, the next call would start a new
deadlock timeout with a later deadline.
An alarm left over from an earlier wait can make this happen repeatedly.
Canceling a logical timeout does not necessarily cancel the kernel
alarm. If that alarm fires before the current deadlock deadline, the
signal handler wakes startup and schedules another alarm for that
deadline. Startup then restarts its deadlock timeout, moving the deadline
past the pending alarm again. This can continue indefinitely, so the
deadlock timeout never fires.
Startup may therefore never ask the conflicting backends to check for
deadlocks. If the applicable max_standby_streaming_delay or
max_standby_archive_delay is -1, there is no delay limit to force
cancellation, and recovery can remain stuck.
Keep the existing timers running when a wakeup does not end the wait.
For buffer pins, check whether other processes still hold pins and,
if necessary, register for another notification when they release them.
For relation locks, check whether startup is still waiting for the lock.
If the conflict remains and neither timeout has expired, wait again
without restarting the timers.
When the standby delay limit has already passed and no timers were set,
continue returning after a wakeup so the caller can retry.
---
src/backend/storage/buffer/bufmgr.c | 102 ++++++++++++++++
src/backend/storage/ipc/standby.c | 178 +++++++++++++++++-----------
src/include/storage/bufmgr.h | 1 +
3 files changed, 213 insertions(+), 68 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f81c7732e68..6733c3c2dcd 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3471,6 +3471,52 @@ WakePinCountWaiter(BufferDesc *buf)
UnlockBufHdr(buf);
}
+/*
+ * Register the current process to be notified when only its own pin remains,
+ * then unlock the buffer header. Return true if the process needs to wait,
+ * or false if the pin count has already dropped to one.
+ *
+ * The caller must hold the buffer header lock, pass the current buffer state
+ * returned by LockBufHdr(), and ensure that no other process is already
+ * registered as the waiter.
+ */
+static bool
+RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
+{
+ Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
+ bufHdr->wait_backend_pgprocno == MyProcNumber);
+
+ bufHdr->wait_backend_pgprocno = MyProcNumber;
+ PinCountWaitBuf = bufHdr;
+
+ /*
+ * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
+ * The shared refcount can be decremented while BM_LOCKED is set, so
+ * use an atomic operation that preserves concurrent refcount changes.
+ */
+ pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
+
+ /*
+ * Recheck the refcount after publishing the waiter flag, while shared
+ * refcount increments are still prevented by BM_LOCKED. If only our
+ * pin remains, the cleanup-lock condition has already been satisfied,
+ * so remove the waiter state and return without sleeping.
+ */
+ buf_state = pg_atomic_read_u64(&bufHdr->state);
+ if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
+ {
+ UnlockBufHdrExt(bufHdr, buf_state,
+ 0, BM_PIN_COUNT_WAITER,
+ 0);
+ PinCountWaitBuf = NULL;
+ return false;
+ }
+
+ UnlockBufHdr(bufHdr);
+
+ return true;
+}
+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -6679,6 +6725,62 @@ CheckBufferIsPinnedOnce(Buffer buffer)
}
}
+/*
+ * PinCountWaiterCheckReadyForCleanup
+ * Check whether only our own pin remains and, if necessary, register
+ * for another notification.
+ *
+ * The caller must own the backend-local cleanup wait for this buffer, as
+ * indicated by PinCountWaitBuf. BM_PIN_COUNT_WAITER may still be set for
+ * this process, or it may already have been cleared by WakePinCountWaiter()
+ * before signaling us.
+ *
+ * Return true if our own pin is the only remaining shared pin. Otherwise,
+ * ensure that this process is registered as the shared pin-count waiter and
+ * return false.
+ */
+bool
+PinCountWaiterCheckReadyForCleanup(Buffer buffer)
+{
+ BufferDesc *bufHdr;
+ uint64 buf_state;
+ uint32 buf_refcount;
+
+ Assert(BufferIsValid(buffer));
+ Assert(!BufferIsLocal(buffer));
+
+ bufHdr = GetBufferDescriptor(buffer - 1);
+ Assert(PinCountWaitBuf == bufHdr);
+
+ buf_state = LockBufHdr(bufHdr);
+ buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
+
+ if (buf_refcount == 1)
+ {
+ UnlockBufHdr(bufHdr);
+ return true;
+ }
+
+ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
+ bufHdr->wait_backend_pgprocno != MyProcNumber)
+ {
+ UnlockBufHdr(bufHdr);
+ elog(ERROR, "multiple backends attempting to wait for pincount 1");
+ }
+
+ /*
+ * If other processes still pin the buffer, register this process again as
+ * the pincount waiter to wait again. The refcount may be concurrently
+ * reduced to 1 despite our holding the buffer header lock, in which case
+ * RegisterPinCountWaiter() returns false and the buffer is ready for
+ * cleanup.
+ */
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
+ return true;
+
+ return false;
+}
+
/*
* LockBufferForCleanup - lock a buffer in preparation for deleting items
*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7f011e04990..89319db0311 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -627,6 +627,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
{
TimestampTz ltime;
TimestampTz now;
+ bool timeouts_armed = false;
Assert(InHotStandby);
@@ -697,61 +698,80 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
cnt++;
enable_timeouts(timeouts, cnt);
+ timeouts_armed = true;
}
- /* Wait to be signaled by the release of the Relation Lock */
- ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
-
/*
- * Exit if ltime is reached. Then all the backends holding conflicting
- * locks will be canceled in the next ResolveRecoveryConflictWithLock()
- * call.
+ * ProcWaitForSignal() can wake up even when the lock wait has not ended
+ * and neither timeout has expired. Keep waiting with the same timeouts
+ * in that case. Returning to the caller would restart the deadlock
+ * timeout with a later deadline, which could keep it from ever expiring.
+ *
+ * If the standby delay limit was already reached above, no timeouts were
+ * set. Return after any wakeup so the caller can try resolving the
+ * conflict again.
*/
- if (got_standby_lock_timeout)
- goto cleanup;
-
- if (got_standby_deadlock_timeout)
+ for (;;)
{
- VirtualTransactionId *backends;
+ ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
- backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
+ if (!timeouts_armed)
+ break;
- /* Quick exit if there's no work to be done */
- if (!VirtualTransactionIdIsValid(*backends))
- goto cleanup;
+ if (*((volatile ProcWaitStatus *) &MyProc->waitStatus) != PROC_WAIT_STATUS_WAITING)
+ break;
/*
- * Send signals to all the backends holding the conflicting locks, to
- * ask them to check themselves for deadlocks.
+ * Exit if ltime is reached. Then all the backends holding conflicting
+ * locks will be canceled in the next
+ * ResolveRecoveryConflictWithLock() call.
*/
- while (VirtualTransactionIdIsValid(*backends))
+ if (got_standby_lock_timeout)
+ break;
+
+ if (got_standby_deadlock_timeout)
{
- (void) SignalRecoveryConflictWithVirtualXID(*backends,
- RECOVERY_CONFLICT_STARTUP_DEADLOCK);
- backends++;
- }
+ VirtualTransactionId *backends;
- /*
- * Exit if the recovery conflict has not been logged yet even though
- * logging is enabled, so that the caller can log that. Then
- * RecoveryConflictWithLock() is called again and we will wait again
- * for the lock to be released.
- */
- if (logging_conflict)
- goto cleanup;
+ backends = GetLockConflicts(&locktag, AccessExclusiveLock, NULL);
- /*
- * Wait again here to be signaled by the release of the Relation Lock,
- * to prevent the subsequent RecoveryConflictWithLock() from causing
- * deadlock_timeout and sending a request for deadlocks check again.
- * Otherwise the request continues to be sent every deadlock_timeout
- * until the relation locks are released or ltime is reached.
- */
- got_standby_deadlock_timeout = false;
- ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
- }
+ /* Quick exit if there's no work to be done */
+ if (!VirtualTransactionIdIsValid(*backends))
+ break;
+
+ /*
+ * Send signals to all the backends holding the conflicting locks,
+ * to ask them to check themselves for deadlocks.
+ */
+ while (VirtualTransactionIdIsValid(*backends))
+ {
+ (void) SignalRecoveryConflictWithVirtualXID(*backends,
+ RECOVERY_CONFLICT_STARTUP_DEADLOCK);
+ backends++;
+ }
-cleanup:
+ /*
+ * Exit if the recovery conflict has not been logged yet even
+ * though logging is enabled, so that the caller can log that.
+ * Then RecoveryConflictWithLock() is called again and we will
+ * wait again for the lock to be released.
+ */
+ if (logging_conflict)
+ break;
+
+ /*
+ * Wait again here to be signaled by the release of the Relation
+ * Lock, to prevent the subsequent RecoveryConflictWithLock() from
+ * causing deadlock_timeout and sending a request for deadlocks
+ * check again. Otherwise the request continues to be sent every
+ * deadlock_timeout until the relation locks are released or ltime
+ * is reached.
+ */
+ got_standby_deadlock_timeout = false;
+ ProcWaitForSignal(PG_WAIT_LOCK | locktag.locktag_type);
+ break;
+ }
+ }
/*
* Clear any timeout requests established above. We assume here that the
@@ -790,17 +810,25 @@ cleanup:
* Deadlocks are extremely rare, and relatively expensive to check for,
* so we don't do a deadlock check right away ... only if we have had to wait
* at least deadlock_timeout.
+ *
+ * The current process should be the waiter process and should have
+ * published the waited buffer via SetStartupBufferPinWaitBuf().
*/
void
ResolveRecoveryConflictWithBufferPin(void)
{
TimestampTz ltime;
+ Buffer buffer;
+ bool timeouts_armed = false;
Assert(InHotStandby);
+ buffer = GetStartupBufferPinWaitBuf();
+ Assert(BufferIsValid(buffer));
+
ltime = GetStandbyLimitTime();
- if (GetCurrentTimestamp() >= ltime && ltime != 0)
+ if (ltime != 0 && GetCurrentTimestamp() >= ltime)
{
/*
* We're already behind, so clear a path as quickly as possible.
@@ -831,37 +859,51 @@ ResolveRecoveryConflictWithBufferPin(void)
cnt++;
enable_timeouts(timeouts, cnt);
+ timeouts_armed = true;
}
/*
- * Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
- * by one of the timeouts established above.
- *
- * We assume that only UnpinBuffer() and the timeout requests established
- * above can wake us up here. WakeupRecovery() called by walreceiver or
- * SIGHUP signal handler, etc cannot do that because it uses the different
- * latch from that ProcWaitForSignal() waits on.
+ * ProcWaitForSignal() can wake up for unrelated reasons. If timeouts
+ * were set, keep waiting with the same deadlines until a timeout expires
+ * or only our own pin remains. The helper also makes sure we are
+ * registered for another unpin notification before waiting again. If the
+ * delay limit had already passed, return after any wakeup so the caller
+ * can retry.
*/
- ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
-
- if (got_standby_delay_timeout)
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
- else if (got_standby_deadlock_timeout)
+ for (;;)
{
- /*
- * Send out a request for hot-standby backends to check themselves for
- * deadlocks.
- *
- * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
- * to be signaled by UnpinBuffer() again and send a request for
- * deadlocks check if deadlock_timeout happens. This causes the
- * request to continue to be sent every deadlock_timeout until the
- * buffer is unpinned or ltime is reached. This would increase the
- * workload in the startup process and backends. In practice it may
- * not be so harmful because the period that the buffer is kept pinned
- * is basically no so long. But we should fix this?
- */
- SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
+
+ if (!timeouts_armed)
+ break;
+
+ if (PinCountWaiterCheckReadyForCleanup(buffer))
+ break;
+
+ /* Request cancellation if the delay timeout has expired. */
+ if (got_standby_delay_timeout)
+ {
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
+ break;
+ }
+ else if (got_standby_deadlock_timeout)
+ {
+ /*
+ * Send out a request for hot-standby backends to check themselves
+ * for deadlocks.
+ *
+ * XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
+ * wait to be signaled by UnpinBuffer() again and send a request
+ * for deadlocks check if deadlock_timeout happens. This causes
+ * the request to continue to be sent every deadlock_timeout until
+ * the buffer is unpinned or ltime is reached. This would increase
+ * the workload in the startup process and backends. In practice
+ * it may not be so harmful because the period that the buffer is
+ * kept pinned is basically no so long. But we should fix this?
+ */
+ SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
+ break;
+ }
}
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6837b35fc6d..a3d297fb2ee 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -341,6 +341,7 @@ LockBuffer(Buffer buffer, BufferLockMode mode)
extern bool ConditionalLockBuffer(Buffer buffer);
extern void LockBufferForCleanup(Buffer buffer);
+extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
extern bool ConditionalLockBufferForCleanup(Buffer buffer);
extern bool IsBufferCleanupOK(Buffer buffer);
extern bool HoldingBufferPinThatDelaysRecovery(void);
--
2.51.0
v10-0003-Use-RegisterPinCountWaiter-in-LockBufferForClean.patch (application/octet-stream, 2.3K) download
From 4d2cf56d5ad7b279ba07dca049987613068b2010 Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:32:32 +0800
Subject: [PATCH v10 3/3] Use RegisterPinCountWaiter() in
LockBufferForCleanup()
Replace the duplicated pincount-waiter registration logic in
LockBufferForCleanup() with a call to RegisterPinCountWaiter(), which
already encapsulates the same protocol for publishing
BM_PIN_COUNT_WAITER, rechecking the refcount, and returning false when
only our own pin remains.
---
src/backend/storage/buffer/bufmgr.c | 28 ++++------------------------
1 file changed, 4 insertions(+), 24 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6733c3c2dcd..09189de491c 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6846,34 +6846,14 @@ LockBufferForCleanup(Buffer buffer)
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
elog(ERROR, "multiple backends attempting to wait for pincount 1");
}
- bufHdr->wait_backend_pgprocno = MyProcNumber;
- PinCountWaitBuf = bufHdr;
-
- /*
- * Publish BM_PIN_COUNT_WAITER while retaining the buffer header lock.
- * The shared refcount can be decremented while BM_LOCKED is set, so
- * use an atomic operation that preserves concurrent refcount changes.
- */
- pg_atomic_fetch_or_u64(&bufHdr->state, BM_PIN_COUNT_WAITER);
-
/*
- * Recheck the refcount after publishing the waiter flag, while shared
- * refcount increments are still prevented by BM_LOCKED. If only our
- * pin remains, the cleanup-lock condition has already been satisfied,
- * so remove the waiter state and return without sleeping.
+ * Register ourselves as the pincount waiter. If the shared refcount
+ * was concurrently reduced to 1 (only our own pin remains),
+ * RegisterPinCountWaiter() returns false and no wait is necessary.
*/
- buf_state = pg_atomic_read_u64(&bufHdr->state);
-
- if (BUF_STATE_GET_REFCOUNT(buf_state) == 1)
- {
- UnlockBufHdrExt(bufHdr, buf_state,
- 0, BM_PIN_COUNT_WAITER,
- 0);
- PinCountWaitBuf = NULL;
+ if (!RegisterPinCountWaiter(bufHdr, buf_state))
goto cleanup_lock_acquired;
- }
- UnlockBufHdr(bufHdr);
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/* Wait to be signaled by UnpinBuffer() */
--
2.51.0
v10-0002-Test-recovery-conflict-waits-after-unrelated-wak.patch (application/octet-stream, 8.8K) download
From cac85405c5ff1af70bed78f67b82245f8217f0ae Mon Sep 17 00:00:00 2001
From: alterego655 <824662526@qq.com>
Date: Tue, 1 Sep 2026 20:31:08 +0800
Subject: [PATCH v10 2/3] Test recovery conflict waits after unrelated wakeups
Wake startup with pg_log_backend_memory_contexts() while a standby
session holds a conflicting buffer pin or relation lock. Attach a notice
injection point after the initial timer setup and check that startup
does not reach it again when it resumes waiting.
Set deadlock_timeout to one day so normal expiration does not interfere
with the check. Wait for startup's memory-context log message and then
its conflict wait before checking for a notice.
Also add a relation lock deadlock test. Let the session finish its first
deadlock check before completing the cycle, so startup must request
another check for recovery to continue.
---
src/backend/storage/ipc/standby.c | 2 +
src/test/recovery/t/031_recovery_conflict.pl | 143 ++++++++++++++++++-
2 files changed, 140 insertions(+), 5 deletions(-)
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 89319db0311..6438bca471f 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -699,6 +699,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
enable_timeouts(timeouts, cnt);
timeouts_armed = true;
+ INJECTION_POINT("standby-conflict-timeouts-armed", "lock");
}
/*
@@ -860,6 +861,7 @@ ResolveRecoveryConflictWithBufferPin(void)
enable_timeouts(timeouts, cnt);
timeouts_armed = true;
+ INJECTION_POINT("standby-conflict-timeouts-armed", "bufferpin");
}
/*
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index d3f4f591661..37a830f0ef9 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -223,6 +223,7 @@ $node_standby->adjust_conf(
'postgresql.conf',
'max_standby_streaming_delay',
"${PostgreSQL::Test::Utils::timeout_default}s");
+
$node_standby->restart();
$psql_standby->reconnect_and_clear();
@@ -247,7 +248,7 @@ SELECT txid_current();
$node_primary->wait_for_replay_catchup($node_standby);
-$res = $psql_standby->query_until(
+$psql_standby->query_until(
qr/^1$/m, qq[
BEGIN;
-- hold pin
@@ -256,9 +257,6 @@ $res = $psql_standby->query_until(
-- wait for lock held by prepared transaction
SELECT * FROM $table2;
]);
-ok(1,
- "$sect: cursor holding conflicting pin, also waiting for lock, established"
-);
# just to make sure we're waiting for lock already
ok( $node_standby->poll_query_until(
@@ -276,10 +274,55 @@ check_conflict_log("User transaction caused buffer deadlock with recovery.");
$psql_standby->reconnect_and_clear();
check_conflict_stat("deadlock");
+
+## RECOVERY CONFLICT 6: Lock deadlock
+$sect = "startup lock deadlock";
+
+# Restart to clear any alarm left by the buffer pin test. Disable progress
+# logging so it does not set another timer.
+$node_standby->append_conf('postgresql.conf',
+ 'log_startup_progress_interval = 0');
+$node_standby->restart();
+$psql_standby->reconnect_and_clear();
+
+# Hold a lock on table1 while waiting for startup's lock on table2. Let the
+# session finish its first deadlock check before making startup wait for
+# table1. That first check finds no deadlock, so startup must ask the
+# session to check again once both processes are waiting for each other.
+my $standby_pid = $psql_standby->query_safe('SELECT pg_backend_pid()');
+$log_location = -s $node_standby->logfile;
+$psql_standby->query_until(
+ qr/^1$/m, qq[
+ BEGIN;
+ SET LOCAL deadlock_timeout = '10ms';
+ SET LOCAL log_lock_waits = on;
+ LOCK TABLE $table1 IN ACCESS SHARE MODE;
+ SELECT 1;
+ SELECT * FROM $table2;
+]);
+$node_standby->wait_for_log(
+ qr/process $standby_pid still waiting for AccessShareLock/,
+ $log_location);
+
+# Make startup wait for the session's lock on table1, creating the deadlock.
+$node_primary->safe_psql($test_db, qq[BEGIN; LOCK TABLE $table1; COMMIT;]);
+$node_primary->wait_for_replay_catchup($node_standby);
+
+# Check that the session reports a deadlock and increments the deadlock count.
+$node_standby->wait_for_log(qr/ERROR: deadlock detected/, $log_location);
+$psql_standby->reconnect_and_clear();
+ok( $node_standby->poll_query_until(
+ $test_db,
+ qq[SELECT deadlocks FROM pg_stat_database WHERE datname='$test_db';],
+ '1'),
+ "$sect: stats show lock deadlock on standby");
+
# clean up for next tests
$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
'50ms');
+$node_standby->adjust_conf('postgresql.conf', 'log_startup_progress_interval',
+ undef);
$node_standby->restart();
$psql_standby->reconnect_and_clear();
@@ -297,7 +340,7 @@ cmp_ok( $node_standby->safe_psql(
);
-## RECOVERY CONFLICT 6: Database conflict
+## RECOVERY CONFLICT 7: Database conflict
$sect = "database conflict";
$node_primary->safe_psql('postgres', qq[DROP DATABASE $test_db;]);
@@ -311,6 +354,96 @@ check_conflict_log("User was connected to a database that must be dropped");
# windows
$psql_standby->quit;
+# An unrelated wakeup must not restart the deadlock timer in either recovery
+# wait. Run these tests last because they use a long timeout.
+if ( $ENV{enable_injection_points} eq 'yes'
+ && $node_primary->check_extension('injection_points'))
+{
+ $node_primary->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ $node_standby->adjust_conf('postgresql.conf',
+ 'max_standby_streaming_delay', '-1');
+ $node_standby->append_conf('postgresql.conf',
+ 'log_startup_progress_interval = 0');
+ # Set the deadlock timeout far beyond the expected test duration.
+ $node_standby->adjust_conf('postgresql.conf', 'deadlock_timeout', '1d');
+
+ # Log startup's notices so we can see if it sets the timer again.
+ $node_standby->append_conf('postgresql.conf',
+ q[log_min_messages = 'warning, startup:notice']);
+ $node_standby->restart();
+
+ my $startup_pid = $node_standby->safe_psql('postgres',
+ q[SELECT pid FROM pg_stat_activity WHERE backend_type = 'startup']);
+ my $point = 'standby-conflict-timeouts-armed';
+
+ for my $kind ('bufferpin', 'lock')
+ {
+ my $table = "test_recovery_wakeup_$kind";
+ my $wait_event = $kind eq 'bufferpin' ? 'BufferCleanup' : 'relation';
+
+ # Leave aborted rows for VACUUM to remove. Commit another insert to
+ # flush the WAL, then replay it before taking the cursor's snapshot.
+ $node_primary->safe_psql(
+ 'postgres', qq[
+CREATE TABLE $table(a int) WITH (autovacuum_enabled = false);
+INSERT INTO $table VALUES (1);
+BEGIN;
+INSERT INTO $table SELECT generate_series(1, 100);
+ROLLBACK;
+INSERT INTO $table VALUES (2);
+]);
+ $node_primary->wait_for_replay_catchup($node_standby);
+
+ my $session = $node_standby->background_psql('postgres');
+ $session->query_safe(
+ $kind eq 'bufferpin'
+ ? "BEGIN; DECLARE c CURSOR FOR SELECT a FROM $table; FETCH FROM c;"
+ : "BEGIN; LOCK TABLE $table IN ACCESS SHARE MODE;");
+
+ $node_primary->safe_psql('postgres',
+ $kind eq 'bufferpin'
+ ? "VACUUM FREEZE $table"
+ : "BEGIN; LOCK TABLE $table; COMMIT;");
+ $node_standby->wait_for_event('startup', $wait_event);
+
+ # The timer is already set. Log any attempt by this resolver to set it
+ # again.
+ $node_standby->safe_psql('postgres',
+ "SELECT injection_points_attach('$point', 'notice', '$kind');");
+
+ # Wake startup while the session still holds the pin or lock. Wait for
+ # its log message so we know it has left the original wait.
+ my $log_offset = -s $node_standby->logfile;
+ is( $node_standby->safe_psql(
+ 'postgres',
+ "SELECT pg_log_backend_memory_contexts($startup_pid)"),
+ 't',
+ "$kind: memory-context request sent to startup");
+ $node_standby->wait_for_log(
+ qr/logging memory contexts of PID $startup_pid\b/, $log_offset);
+
+ # Wait for startup to resume its wait for the pin or lock. Startup
+ # writes notices directly to the test log, so any notice from setting
+ # the timer again must be in the file by then.
+ $node_standby->wait_for_event('startup', $wait_event);
+ unlike(
+ slurp_file($node_standby->logfile, $log_offset),
+ qr/notice triggered for injection point \Q$point\E/,
+ "$kind: unrelated wakeup did not restart the deadlock timer");
+
+ # Stop recording timer setup before releasing the pin or lock.
+ $node_standby->safe_psql('postgres',
+ "SELECT injection_points_detach('$point');");
+ $session->query_safe('COMMIT;');
+ # Wait for replay too: VACUUM can return before flushing its WAL.
+ $node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+ $session->quit;
+ }
+}
+
$node_standby->stop();
$node_primary->stop();
--
2.51.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-28 02:16 shihao zhong <zhong950419@gmail.com>
parent: Xuneng Zhou <xunengzhou@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: shihao zhong @ 2026-10-28 02:16 UTC (permalink / raw)
To: Xuneng Zhou <xunengzhou@gmail.com>; +Cc: Vitaly Davydov <vitprof@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
Hi Xuneng,
Thanks for v10 and for finding the lock case. I tested it and have
a few notes.
1. I am able to reproduce the case with your code and when
it passed deadlock_timeout. And your V10 is able to cancel the
deadlocked query.
2. V10 leaves the case where the delay has already passed as it was,
one wait with no timeout. So the problem in CF 6445 [1] is still
there.
I think this patch is LGTM.
[1] https://commitfest.postgresql.org/patch/6445/
Thanks,
Shihao
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Deadlock detector fails to activate on a hot standby replica
@ 2026-10-28 09:32 Xuneng Zhou <xunengzhou@gmail.com>
parent: shihao zhong <zhong950419@gmail.com>
0 siblings, 0 replies; 43+ messages in thread
From: Xuneng Zhou @ 2026-10-28 09:32 UTC (permalink / raw)
To: shihao zhong <zhong950419@gmail.com>; +Cc: Vitaly Davydov <vitprof@gmail.com>; pgsql-hackers@lists.postgresql.org, Fujii Masao <masao.fujii@gmail.com>; JoongHyuk Shin <sjh910805@gmail.com>
Hi shihao,
On Wed, Oct 28, 2026 at 10:16 AM shihao zhong <zhong950419@gmail.com> wrote:
> Hi Xuneng,
>
> Thanks for v10 and for finding the lock case. I tested it and have
> a few notes.
>
> 1. I am able to reproduce the case with your code and when
> it passed deadlock_timeout. And your V10 is able to cancel the
> deadlocked query.
>
> 2. V10 leaves the case where the delay has already passed as it was,
> one wait with no timeout. So the problem in CF 6445 [1] is still
> there.
>
> I think this patch is LGTM.
>
> [1] https://commitfest.postgresql.org/patch/6445/
>
Thanks for your review! I am aware of the patch you referred to and plan to
take a look soon.
--
Regards,
Xuneng Zhou
HighGo Software Co., Ltd.
^ permalink raw reply [nested|flat] 43+ messages in thread
end of thread, other threads:[~2026-10-28 09:32 UTC | newest]
Thread overview: 43+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-19 12:43 Deadlock detector fails to activate on a hot standby replica Vitaly Davydov <v.davydov@postgrespro.ru>
2026-01-23 11:51 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-05-20 16:26 ` Fujii Masao <masao.fujii@gmail.com>
2026-05-25 14:20 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-05-26 15:01 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-05 11:58 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-09 16:18 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-10 08:42 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-12 06:00 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-12 09:23 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-18 02:48 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-18 13:11 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-18 16:22 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-22 18:32 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-23 03:13 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-24 15:16 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-25 12:14 ` Fujii Masao <masao.fujii@gmail.com>
2026-06-25 13:36 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-06-26 09:31 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-06-27 06:11 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-06-30 03:14 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-07-01 15:57 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-07-02 09:31 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-08-04 05:30 ` JoongHyuk Shin <sjh910805@gmail.com>
2026-08-04 11:16 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-08-05 02:40 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-08-05 08:10 ` Vitaly Davydov <v.davydov@postgrespro.ru>
2026-08-08 13:51 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-08-13 17:35 ` Vitaly Davydov <vitprof@gmail.com>
2026-08-17 13:51 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-08-27 10:35 ` JoongHyuk Shin <sjh910805@gmail.com>
2026-08-28 08:17 ` JoongHyuk Shin <sjh910805@gmail.com>
2026-08-31 16:18 ` Vitaly Davydov <vitprof@gmail.com>
2026-08-31 16:27 ` Vitaly Davydov <vitprof@gmail.com>
2026-09-01 07:04 ` JoongHyuk Shin <sjh910805@gmail.com>
2026-09-01 12:52 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-09-04 14:15 ` Vitaly Davydov <vitprof@gmail.com>
2026-10-10 09:24 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-10-10 09:27 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-10-10 09:31 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-10-28 02:16 ` shihao zhong <zhong950419@gmail.com>
2026-10-28 09:32 ` Xuneng Zhou <xunengzhou@gmail.com>
2026-10-02 03:00 ` Xuneng Zhou <xunengzhou@gmail.com>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox