Skip to content

Commit ad2e041

Browse files
committed
Fix multiple bugs in index page locking during hot-standby WAL replay.
In ordinary operation, VACUUM must be careful to take a cleanup lock on each leaf page of a btree index; this ensures that no indexscans could still be "in flight" to heap tuples due to be deleted. (Because of possible index-tuple motion due to concurrent page splits, it's not enough to lock only the pages we're deleting index tuples from.) In Hot Standby, the WAL replay process must likewise lock every leaf page. There were several bugs in the code for that: * The replay scan might come across unused, all-zero pages in the index. While btree_xlog_vacuum itself did the right thing (ie, nothing) with such pages, xlogutils.c supposed that such pages must be corrupt and would throw an error. This accounts for various reports of replication failures with "PANIC: WAL contains references to invalid pages". To fix, add a ReadBufferMode value that instructs XLogReadBufferExtended not to complain when we're doing this. * btree_xlog_vacuum performed the extra locking if standbyState == STANDBY_SNAPSHOT_READY, but that's not the correct test: we won't open up for hot standby queries until the database has reached consistency, and we don't want to do the extra locking till then either, for fear of reading corrupted pages (which bufmgr.c would complain about). Fix by exporting a new function from xlog.c that will report whether we're actually in hot standby replay mode. * To ensure full coverage of the index in the replay scan, btvacuumscan would emit a dummy WAL record for the last page of the index, if no vacuuming work had been done on that page. However, if the last page of the index is all-zero, that would result in corruption of said page, since the functions called on it weren't prepared to handle that case. There's no need to lock any such pages, so change the logic to target the last normal leaf page instead. The first two of these bugs were diagnosed by Andres Freund, the other one by me. Fixes based on ideas from Heikki Linnakangas and myself. This has been wrong since Hot Standby was introduced, so back-patch to 9.0.
1 parent 299498d commit ad2e041

File tree

7 files changed

+98
-42
lines changed

7 files changed

+98
-42
lines changed

src/backend/access/nbtree/nbtree.c

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ typedef struct
5555
IndexBulkDeleteCallback callback;
5656
void *callback_state;
5757
BTCycleId cycleid;
58-
BlockNumber lastBlockVacuumed; /* last blkno reached by Vacuum scan */
59-
BlockNumber lastUsedPage; /* blkno of last non-recyclable page */
58+
BlockNumber lastBlockVacuumed; /* highest blkno actually vacuumed */
59+
BlockNumber lastBlockLocked; /* highest blkno we've cleanup-locked */
6060
BlockNumber totFreePages; /* true total # of free pages */
6161
MemoryContext pagedelcontext;
6262
} BTVacState;
@@ -761,7 +761,7 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
761761
vstate.callback_state = callback_state;
762762
vstate.cycleid = cycleid;
763763
vstate.lastBlockVacuumed = BTREE_METAPAGE; /* Initialise at first block */
764-
vstate.lastUsedPage = BTREE_METAPAGE;
764+
vstate.lastBlockLocked = BTREE_METAPAGE;
765765
vstate.totFreePages = 0;
766766

767767
/* Create a temporary memory context to run _bt_pagedel in */
@@ -817,27 +817,30 @@ btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
817817
}
818818

819819
/*
820-
* InHotStandby we need to scan right up to the end of the index for
821-
* correct locking, so we may need to write a WAL record for the final
822-
* block in the index if it was not vacuumed. It's possible that VACUUMing
823-
* has actually removed zeroed pages at the end of the index so we need to
824-
* take care to issue the record for last actual block and not for the
825-
* last block that was scanned. Ignore empty indexes.
820+
* If the WAL is replayed in hot standby, the replay process needs to get
821+
* cleanup locks on all index leaf pages, just as we've been doing here.
822+
* However, we won't issue any WAL records about pages that have no items
823+
* to be deleted. For pages between pages we've vacuumed, the replay code
824+
* will take locks under the direction of the lastBlockVacuumed fields in
825+
* the XLOG_BTREE_VACUUM WAL records. To cover pages after the last one
826+
* we vacuum, we need to issue a dummy XLOG_BTREE_VACUUM WAL record
827+
* against the last leaf page in the index, if that one wasn't vacuumed.
826828
*/
827829
if (XLogStandbyInfoActive() &&
828-
num_pages > 1 && vstate.lastBlockVacuumed < (num_pages - 1))
830+
vstate.lastBlockVacuumed < vstate.lastBlockLocked)
829831
{
830832
Buffer buf;
831833

832834
/*
833-
* We can't use _bt_getbuf() here because it always applies
834-
* _bt_checkpage(), which will barf on an all-zero page. We want to
835-
* recycle all-zero pages, not fail. Also, we want to use a
836-
* nondefault buffer access strategy.
835+
* The page should be valid, but we can't use _bt_getbuf() because we
836+
* want to use a nondefault buffer access strategy. Since we aren't
837+
* going to delete any items, getting cleanup lock again is probably
838+
* overkill, but for consistency do that anyway.
837839
*/
838-
buf = ReadBufferExtended(rel, MAIN_FORKNUM, num_pages - 1, RBM_NORMAL,
839-
info->strategy);
840+
buf = ReadBufferExtended(rel, MAIN_FORKNUM, vstate.lastBlockLocked,
841+
RBM_NORMAL, info->strategy);
840842
LockBufferForCleanup(buf);
843+
_bt_checkpage(rel, buf);
841844
_bt_delitems_vacuum(rel, buf, NULL, 0, vstate.lastBlockVacuumed);
842845
_bt_relbuf(rel, buf);
843846
}
@@ -912,10 +915,6 @@ btvacuumpage(BTVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno)
912915
}
913916
}
914917

915-
/* If the page is in use, update lastUsedPage */
916-
if (!_bt_page_recyclable(page) && vstate->lastUsedPage < blkno)
917-
vstate->lastUsedPage = blkno;
918-
919918
/* Page is valid, see what to do with it */
920919
if (_bt_page_recyclable(page))
921920
{
@@ -951,6 +950,13 @@ btvacuumpage(BTVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno)
951950
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
952951
LockBufferForCleanup(buf);
953952

953+
/*
954+
* Remember highest leaf page number we've taken cleanup lock on; see
955+
* notes in btvacuumscan
956+
*/
957+
if (blkno > vstate->lastBlockLocked)
958+
vstate->lastBlockLocked = blkno;
959+
954960
/*
955961
* Check whether we need to recurse back to earlier pages. What we
956962
* are concerned about is a page split that happened since we started
@@ -1017,19 +1023,26 @@ btvacuumpage(BTVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno)
10171023
*/
10181024
if (ndeletable > 0)
10191025
{
1020-
BlockNumber lastBlockVacuumed = BufferGetBlockNumber(buf);
1021-
1026+
/*
1027+
* Notice that the issued XLOG_BTREE_VACUUM WAL record includes an
1028+
* instruction to the replay code to get cleanup lock on all pages
1029+
* between the previous lastBlockVacuumed and this page. This
1030+
* ensures that WAL replay locks all leaf pages at some point.
1031+
*
1032+
* Since we can visit leaf pages out-of-order when recursing,
1033+
* replay might end up locking such pages an extra time, but it
1034+
* doesn't seem worth the amount of bookkeeping it'd take to avoid
1035+
* that.
1036+
*/
10221037
_bt_delitems_vacuum(rel, buf, deletable, ndeletable,
10231038
vstate->lastBlockVacuumed);
10241039

10251040
/*
1026-
* Keep track of the block number of the lastBlockVacuumed, so we
1027-
* can scan those blocks as well during WAL replay. This then
1028-
* provides concurrency protection and allows btrees to be used
1029-
* while in recovery.
1041+
* Remember highest leaf page number we've issued a
1042+
* XLOG_BTREE_VACUUM WAL record for.
10301043
*/
1031-
if (lastBlockVacuumed > vstate->lastBlockVacuumed)
1032-
vstate->lastBlockVacuumed = lastBlockVacuumed;
1044+
if (blkno > vstate->lastBlockVacuumed)
1045+
vstate->lastBlockVacuumed = blkno;
10331046

10341047
stats->tuples_removed += ndeletable;
10351048
/* must recompute maxoff */

src/backend/access/nbtree/nbtxlog.c

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -486,28 +486,47 @@ btree_xlog_vacuum(XLogRecPtr lsn, XLogRecord *record)
486486
BTPageOpaque opaque;
487487

488488
/*
489-
* If queries might be active then we need to ensure every block is
489+
* If queries might be active then we need to ensure every leaf page is
490490
* unpinned between the lastBlockVacuumed and the current block, if there
491-
* are any. This ensures that every block in the index is touched during
492-
* VACUUM as required to ensure scans work correctly.
491+
* are any. This prevents replay of the VACUUM from reaching the stage of
492+
* removing heap tuples while there could still be indexscans "in flight"
493+
* to those particular tuples (see nbtree/README).
494+
*
495+
* It might be worth checking if there are actually any backends running;
496+
* if not, we could just skip this.
497+
*
498+
* Since VACUUM can visit leaf pages out-of-order, it might issue records
499+
* with lastBlockVacuumed >= block; that's not an error, it just means
500+
* nothing to do now.
501+
*
502+
* Note: since we touch all pages in the range, we will lock non-leaf
503+
* pages, and also any empty (all-zero) pages that may be in the index. It
504+
* doesn't seem worth the complexity to avoid that. But it's important
505+
* that HotStandbyActiveInReplay() will not return true if the database
506+
* isn't yet consistent; so we need not fear reading still-corrupt blocks
507+
* here during crash recovery.
493508
*/
494-
if (standbyState == STANDBY_SNAPSHOT_READY &&
495-
(xlrec->lastBlockVacuumed + 1) != xlrec->block)
509+
if (HotStandbyActiveInReplay())
496510
{
497-
BlockNumber blkno = xlrec->lastBlockVacuumed + 1;
511+
BlockNumber blkno;
498512

499-
for (; blkno < xlrec->block; blkno++)
513+
for (blkno = xlrec->lastBlockVacuumed + 1; blkno < xlrec->block; blkno++)
500514
{
501515
/*
516+
* We use RBM_NORMAL_NO_LOG mode because it's not an error
517+
* condition to see all-zero pages. The original btvacuumpage
518+
* scan would have skipped over all-zero pages, noting them in FSM
519+
* but not bothering to initialize them just yet; so we mustn't
520+
* throw an error here. (We could skip acquiring the cleanup lock
521+
* if PageIsNew, but it's probably not worth the cycles to test.)
522+
*
502523
* XXX we don't actually need to read the block, we just need to
503524
* confirm it is unpinned. If we had a special call into the
504525
* buffer manager we could optimise this so that if the block is
505526
* not in shared_buffers we confirm it as unpinned.
506-
*
507-
* Another simple optimization would be to check if there's any
508-
* backends running; if not, we could just skip this.
509527
*/
510-
buffer = XLogReadBufferExtended(xlrec->node, MAIN_FORKNUM, blkno, RBM_NORMAL);
528+
buffer = XLogReadBufferExtended(xlrec->node, MAIN_FORKNUM, blkno,
529+
RBM_NORMAL_NO_LOG);
511530
if (BufferIsValid(buffer))
512531
{
513532
LockBufferForCleanup(buffer);

src/backend/access/transam/xlog.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7438,7 +7438,8 @@ RecoveryInProgress(void)
74387438
* true. Postmaster knows this by way of signal, not via shared memory.
74397439
*
74407440
* Unlike testing standbyState, this works in any process that's connected to
7441-
* shared memory.
7441+
* shared memory. (And note that standbyState alone doesn't tell the truth
7442+
* anyway.)
74427443
*/
74437444
bool
74447445
HotStandbyActive(void)
@@ -7464,6 +7465,17 @@ HotStandbyActive(void)
74647465
}
74657466
}
74667467

7468+
/*
7469+
* Like HotStandbyActive(), but to be used only in WAL replay code,
7470+
* where we don't need to ask any other process what the state is.
7471+
*/
7472+
bool
7473+
HotStandbyActiveInReplay(void)
7474+
{
7475+
Assert(AmStartupProcess());
7476+
return LocalHotStandbyActive;
7477+
}
7478+
74677479
/*
74687480
* Is this process allowed to insert new WAL records?
74697481
*

src/backend/access/transam/xlogutils.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ XLogReadBuffer(RelFileNode rnode, BlockNumber blkno, bool init)
287287
*
288288
* In RBM_ZERO and RBM_ZERO_ON_ERROR modes, if the page doesn't exist, the
289289
* relation is extended with all-zeroes pages up to the given block number.
290+
*
291+
* In RBM_NORMAL_NO_LOG mode, we return InvalidBuffer if the page doesn't
292+
* exist, and we don't check for all-zeroes. Thus, no log entry is made
293+
* to imply that the page should be dropped or truncated later.
290294
*/
291295
Buffer
292296
XLogReadBufferExtended(RelFileNode rnode, ForkNumber forknum,
@@ -327,6 +331,8 @@ XLogReadBufferExtended(RelFileNode rnode, ForkNumber forknum,
327331
log_invalid_page(rnode, forknum, blkno, false);
328332
return InvalidBuffer;
329333
}
334+
if (mode == RBM_NORMAL_NO_LOG)
335+
return InvalidBuffer;
330336
/* OK to extend the file */
331337
/* we do this in recovery only - no rel-extension lock needed */
332338
Assert(InRecovery);

src/backend/storage/buffer/bufmgr.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
203203
* Assume when this function is called, that reln has been opened already.
204204
*
205205
* In RBM_NORMAL mode, the page is read from disk, and the page header is
206-
* validated. An error is thrown if the page header is not valid.
206+
* validated. An error is thrown if the page header is not valid. (But
207+
* note that an all-zero page is considered "valid"; see PageIsVerified().)
207208
*
208209
* RBM_ZERO_ON_ERROR is like the normal mode, but if the page header is not
209210
* valid, the page is zeroed instead of throwing an error. This is intended
@@ -217,6 +218,8 @@ ReadBuffer(Relation reln, BlockNumber blockNum)
217218
* current physical EOF; that is likely to cause problems in md.c when
218219
* the page is modified and written out. P_NEW is OK, though.
219220
*
221+
* RBM_NORMAL_NO_LOG mode is treated the same as RBM_NORMAL here.
222+
*
220223
* If strategy is not NULL, a nondefault buffer access strategy is used.
221224
* See buffer/README for details.
222225
*/

src/include/access/xlog.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ extern void issue_xlog_fsync(int fd, uint32 log, uint32 seg);
289289

290290
extern bool RecoveryInProgress(void);
291291
extern bool HotStandbyActive(void);
292+
extern bool HotStandbyActiveInReplay(void);
292293
extern bool XLogInsertAllowed(void);
293294
extern void GetXLogReceiptTime(TimestampTz *rtime, bool *fromStream);
294295
extern XLogRecPtr GetXLogReplayRecPtr(TimeLineID *targetTLI);

src/include/storage/bufmgr.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ typedef enum
3838
RBM_NORMAL, /* Normal read */
3939
RBM_ZERO, /* Don't read from disk, caller will
4040
* initialize */
41-
RBM_ZERO_ON_ERROR /* Read, but return an all-zeros page on error */
41+
RBM_ZERO_ON_ERROR, /* Read, but return an all-zeros page on error */
42+
RBM_NORMAL_NO_LOG /* Don't log page as invalid during WAL
43+
* replay; otherwise same as RBM_NORMAL */
4244
} ReadBufferMode;
4345

4446
/* in globals.c ... this duplicates miscadmin.h */

0 commit comments

Comments
 (0)