Skip to content

Commit 034ef15

Browse files
Vitaly Davydovhackorum
authored andcommitted
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.
1 parent f57df20 commit 034ef15

3 files changed

Lines changed: 132 additions & 26 deletions

File tree

src/backend/storage/buffer/bufmgr.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3469,6 +3469,25 @@ WakePinCountWaiter(BufferDesc *buf)
34693469
UnlockBufHdr(buf);
34703470
}
34713471

3472+
/*
3473+
* Register the current process as the pincount waiter for a shared buffer,
3474+
* set the BM_PIN_COUNT_WAITER flag, and unlock the buffer header.
3475+
*
3476+
* The caller must hold the buffer header lock, pass the current buffer state
3477+
* returned by LockBufHdr(), and ensure that no other backend is already
3478+
* registered as the waiter.
3479+
*/
3480+
static void
3481+
RegisterPinCountWaiter(BufferDesc *bufHdr, uint64 buf_state)
3482+
{
3483+
Assert((buf_state & BM_PIN_COUNT_WAITER) == 0 ||
3484+
bufHdr->wait_backend_pgprocno == MyProcNumber);
3485+
3486+
bufHdr->wait_backend_pgprocno = MyProcNumber;
3487+
PinCountWaitBuf = bufHdr;
3488+
UnlockBufHdrExt(bufHdr, buf_state, BM_PIN_COUNT_WAITER, 0, 0);
3489+
}
3490+
34723491
/*
34733492
* UnpinBuffer -- make buffer available for replacement.
34743493
*
@@ -4763,6 +4782,60 @@ BufferGetLSNAtomic(Buffer buffer)
47634782
#endif
47644783
}
47654784

4785+
/*
4786+
* PinCountWaiterCheckReadyForCleanup
4787+
* Recheck whether pin count waiter process (the startup process)
4788+
* can retry cleanup lock acquisition.
4789+
*
4790+
* This is only for the hot-standby path in LockBufferForCleanup(), via
4791+
* ResolveRecoveryConflictWithBufferPin(), after ProcWaitForSignal() returns.
4792+
* The caller must already be registered as the shared buffer's
4793+
* BM_PIN_COUNT_WAITER.
4794+
*
4795+
* Returns true when the caller itself is the only remaining pin holder, so it
4796+
* can retry taking the cleanup lock. Returns false if other backends still
4797+
* pin the shared buffer. In that case, this function guarantees that the
4798+
* current backend remains registered as the pincount waiter to be woken when
4799+
* the buffer refcount drops to 1.
4800+
*/
4801+
bool
4802+
PinCountWaiterCheckReadyForCleanup(Buffer buffer)
4803+
{
4804+
BufferDesc *bufHdr;
4805+
uint64 buf_state;
4806+
uint32 buf_refcount;
4807+
4808+
Assert(BufferIsValid(buffer));
4809+
Assert(!BufferIsLocal(buffer));
4810+
4811+
bufHdr = GetBufferDescriptor(buffer - 1);
4812+
Assert(PinCountWaitBuf == bufHdr);
4813+
4814+
buf_state = LockBufHdr(bufHdr);
4815+
buf_refcount = BUF_STATE_GET_REFCOUNT(buf_state);
4816+
4817+
if (buf_refcount == 1)
4818+
{
4819+
UnlockBufHdr(bufHdr);
4820+
return true;
4821+
}
4822+
4823+
if ((buf_state & BM_PIN_COUNT_WAITER) != 0 &&
4824+
bufHdr->wait_backend_pgprocno != MyProcNumber)
4825+
{
4826+
UnlockBufHdr(bufHdr);
4827+
elog(ERROR, "multiple processes attempting to wait for pincount 1");
4828+
}
4829+
4830+
/*
4831+
* If other processes still pin the buffer, register this process again as
4832+
* the pincount waiter to wait again.
4833+
*/
4834+
RegisterPinCountWaiter(bufHdr, buf_state);
4835+
4836+
return false;
4837+
}
4838+
47664839
/* ---------------------------------------------------------------------
47674840
* DropRelationBuffers
47684841
*

src/backend/storage/ipc/standby.c

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -790,22 +790,36 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict)
790790
* Deadlocks are extremely rare, and relatively expensive to check for,
791791
* so we don't do a deadlock check right away ... only if we have had to wait
792792
* at least deadlock_timeout.
793+
*
794+
* The current process should be the waiter process and should have
795+
* published the waited buffer via SetStartupBufferPinWaitBufId().
793796
*/
794797
void
795798
ResolveRecoveryConflictWithBufferPin(void)
796799
{
797800
TimestampTz ltime;
801+
int bufid;
798802

799803
Assert(InHotStandby);
800804

805+
bufid = GetStartupBufferPinWaitBufId();
806+
Assert(bufid >= 0);
807+
801808
ltime = GetStandbyLimitTime();
802809

803-
if (GetCurrentTimestamp() >= ltime && ltime != 0)
810+
if (ltime != 0 && GetCurrentTimestamp() >= ltime)
804811
{
805812
/*
806813
* We're already behind, so clear a path as quickly as possible.
807814
*/
808815
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
816+
817+
/*
818+
* Set delay timeout flag once the timeout is reached (the current
819+
* timestamp is greater than the standby limit time). This variable is
820+
* use by timeout handlers with the same purpose.
821+
*/
822+
got_standby_delay_timeout = true;
809823
}
810824
else
811825
{
@@ -833,35 +847,53 @@ ResolveRecoveryConflictWithBufferPin(void)
833847
enable_timeouts(timeouts, cnt);
834848
}
835849

836-
/*
837-
* Wait to be signaled by UnpinBuffer() or for the wait to be interrupted
838-
* by one of the timeouts established above.
839-
*
840-
* We assume that only UnpinBuffer() and the timeout requests established
841-
* above can wake us up here. WakeupRecovery() called by walreceiver or
842-
* SIGHUP signal handler, etc cannot do that because it uses the different
843-
* latch from that ProcWaitForSignal() waits on.
844-
*/
845-
ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
846-
847-
if (got_standby_delay_timeout)
848-
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
849-
else if (got_standby_deadlock_timeout)
850+
for (;;)
850851
{
851852
/*
852-
* Send out a request for hot-standby backends to check themselves for
853-
* deadlocks.
853+
* Wait to be signaled by UnpinBuffer() or for the wait to be
854+
* interrupted by one of the timeouts established above.
854855
*
855-
* XXX The subsequent ResolveRecoveryConflictWithBufferPin() will wait
856-
* to be signaled by UnpinBuffer() again and send a request for
857-
* deadlocks check if deadlock_timeout happens. This causes the
858-
* request to continue to be sent every deadlock_timeout until the
859-
* buffer is unpinned or ltime is reached. This would increase the
860-
* workload in the startup process and backends. In practice it may
861-
* not be so harmful because the period that the buffer is kept pinned
862-
* is basically no so long. But we should fix this?
856+
* ProcWaitForSignal() can also wake up for unrelated reasons, so
857+
* recheck later whether cleanup can proceed.
863858
*/
864-
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
859+
ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP);
860+
861+
/*
862+
* Once the reference count is 1, the waiter process itself is the
863+
* only backend pinning the buffer at the moment. There is a chance to
864+
* lock the buffer exclusively.
865+
*/
866+
if (PinCountWaiterCheckReadyForCleanup(bufid + 1))
867+
break;
868+
869+
/*
870+
* Send the recovery conflict if the standby delay timeout is activated or
871+
* standby limit time was already reached. The second condition handles the
872+
* fast path when timeouts are not activated.
873+
*/
874+
if (got_standby_delay_timeout)
875+
{
876+
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN);
877+
break;
878+
}
879+
else if (got_standby_deadlock_timeout)
880+
{
881+
/*
882+
* Send out a request for hot-standby backends to check themselves
883+
* for deadlocks.
884+
*
885+
* XXX The subsequent ResolveRecoveryConflictWithBufferPin() will
886+
* wait to be signaled by UnpinBuffer() again and send a request
887+
* for deadlocks check if deadlock_timeout happens. This causes
888+
* the request to continue to be sent every deadlock_timeout until
889+
* the buffer is unpinned or ltime is reached. This would increase
890+
* the workload in the startup process and backends. In practice
891+
* it may not be so harmful because the period that the buffer is
892+
* kept pinned is basically no so long. But we should fix this?
893+
*/
894+
SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK);
895+
break;
896+
}
865897
}
866898

867899
/*

src/include/storage/bufmgr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ extern bool BufferIsPermanent(Buffer buffer);
313313
extern XLogRecPtr BufferGetLSNAtomic(Buffer buffer);
314314
extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator,
315315
ForkNumber *forknum, BlockNumber *blknum);
316+
extern bool PinCountWaiterCheckReadyForCleanup(Buffer buffer);
316317

317318
extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std);
318319

0 commit comments

Comments
 (0)