Commit 0d1fe9f7 authored by Andres Freund's avatar Andres Freund

Move page initialization from RelationAddExtraBlocks() to use, take 2.

Previously we initialized pages when bulk extending in
RelationAddExtraBlocks(). That has a major disadvantage: It ties
RelationAddExtraBlocks() to heap, as other types of storage are likely
to need different amounts of special space, have different amount of
free space (previously determined by PageGetHeapFreeSpace()).

That we're relying on initializing pages, but not WAL logging the
initialization, also means the risk for getting
"WARNING:  relation \"%s\" page %u is uninitialized --- fixing"
style warnings in vacuums after crashes/immediate shutdowns, is
considerably higher. The warning sounds much more serious than what
they are.

Fix those two issues together by not initializing pages in
RelationAddExtraPages() (but continue to do so in
RelationGetBufferForTuple(), which is linked much more closely to
heap), and accepting uninitialized pages as normal in
vacuumlazy.c. When vacuumlazy encounters an empty page it now adds it
to the FSM, but does nothing else.  We chose to not issue a debug
message, much less a warning in that case - it seems rarely useful,
and quite likely to scare people unnecessarily.

For now empty pages aren't added to the VM, because standbys would not
re-discover such pages after a promotion. In contrast to other sources
for empty pages, there's no corresponding WAL records triggering FSM
updates during replay.

Previously when extending the relation, there was a moment between
extending the relation, and acquiring an exclusive lock on the new
page, in which another backend could lock the page. To avoid new
content being put on that new page, vacuumlazy needed to acquire the
extension lock for a brief moment when encountering a new page. A
second corner case, only working somewhat by accident, was that
RelationGetBufferForTuple() sometimes checks the last page in a
relation for free space, without consulting the FSM; that only worked
because PageGetHeapFreeSpace() interprets the zero page header in a
new page as no free space.  The lack of handling this properly
required reverting the previous attempt in 68420054.

This issue can be solved by using RBM_ZERO_AND_LOCK when extending the
relation, thereby avoiding this window. There's some added complexity
when RelationGetBufferForTuple() is called with another buffer (for
updates), to avoid deadlocks, but that's rarely hit at runtime.

Author: Andres Freund
Reviewed-By: Tom Lane
Discussion: https://postgr.es/m/20181219083945.6khtgm36mivonhva@alap3.anarazel.de
parent ac3a9afd
...@@ -74,23 +74,31 @@ RelationPutHeapTuple(Relation relation, ...@@ -74,23 +74,31 @@ RelationPutHeapTuple(Relation relation,
} }
/* /*
* Read in a buffer, using bulk-insert strategy if bistate isn't NULL. * Read in a buffer in mode, using bulk-insert strategy if bistate isn't NULL.
*/ */
static Buffer static Buffer
ReadBufferBI(Relation relation, BlockNumber targetBlock, ReadBufferBI(Relation relation, BlockNumber targetBlock,
BulkInsertState bistate) ReadBufferMode mode, BulkInsertState bistate)
{ {
Buffer buffer; Buffer buffer;
/* If not bulk-insert, exactly like ReadBuffer */ /* If not bulk-insert, exactly like ReadBuffer */
if (!bistate) if (!bistate)
return ReadBuffer(relation, targetBlock); return ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
mode, NULL);
/* If we have the desired block already pinned, re-pin and return it */ /* If we have the desired block already pinned, re-pin and return it */
if (bistate->current_buf != InvalidBuffer) if (bistate->current_buf != InvalidBuffer)
{ {
if (BufferGetBlockNumber(bistate->current_buf) == targetBlock) if (BufferGetBlockNumber(bistate->current_buf) == targetBlock)
{ {
/*
* Currently the LOCK variants are only used for extending
* relation, which should never reach this branch.
*/
Assert(mode != RBM_ZERO_AND_LOCK &&
mode != RBM_ZERO_AND_CLEANUP_LOCK);
IncrBufferRefCount(bistate->current_buf); IncrBufferRefCount(bistate->current_buf);
return bistate->current_buf; return bistate->current_buf;
} }
...@@ -101,7 +109,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock, ...@@ -101,7 +109,7 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock,
/* Perform a read using the buffer strategy */ /* Perform a read using the buffer strategy */
buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock, buffer = ReadBufferExtended(relation, MAIN_FORKNUM, targetBlock,
RBM_NORMAL, bistate->strategy); mode, bistate->strategy);
/* Save the selected block as target for future inserts */ /* Save the selected block as target for future inserts */
IncrBufferRefCount(buffer); IncrBufferRefCount(buffer);
...@@ -204,11 +212,10 @@ RelationAddExtraBlocks(Relation relation, BulkInsertState bistate) ...@@ -204,11 +212,10 @@ RelationAddExtraBlocks(Relation relation, BulkInsertState bistate)
/* /*
* Extend by one page. This should generally match the main-line * Extend by one page. This should generally match the main-line
* extension code in RelationGetBufferForTuple, except that we hold * extension code in RelationGetBufferForTuple, except that we hold
* the relation extension lock throughout. * the relation extension lock throughout, and we don't immediately
* initialize the page (see below).
*/ */
buffer = ReadBufferBI(relation, P_NEW, bistate); buffer = ReadBufferBI(relation, P_NEW, RBM_ZERO_AND_LOCK, bistate);
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
page = BufferGetPage(buffer); page = BufferGetPage(buffer);
if (!PageIsNew(page)) if (!PageIsNew(page))
...@@ -216,18 +223,18 @@ RelationAddExtraBlocks(Relation relation, BulkInsertState bistate) ...@@ -216,18 +223,18 @@ RelationAddExtraBlocks(Relation relation, BulkInsertState bistate)
BufferGetBlockNumber(buffer), BufferGetBlockNumber(buffer),
RelationGetRelationName(relation)); RelationGetRelationName(relation));
PageInit(page, BufferGetPageSize(buffer), 0);
/* /*
* We mark all the new buffers dirty, but do nothing to write them * Add the page to the FSM without initializing. If we were to
* out; they'll probably get used soon, and even if they are not, a * initialize here, the page would potentially get flushed out to disk
* crash will leave an okay all-zeroes page on disk. * before we add any useful content. There's no guarantee that that'd
* happen before a potential crash, so we need to deal with
* uninitialized pages anyway, thus avoid the potential for
* unnecessary writes.
*/ */
MarkBufferDirty(buffer);
/* we'll need this info below */ /* we'll need this info below */
blockNum = BufferGetBlockNumber(buffer); blockNum = BufferGetBlockNumber(buffer);
freespace = PageGetHeapFreeSpace(page); freespace = BufferGetPageSize(buffer) - SizeOfPageHeaderData;
UnlockReleaseBuffer(buffer); UnlockReleaseBuffer(buffer);
...@@ -412,7 +419,7 @@ loop: ...@@ -412,7 +419,7 @@ loop:
if (otherBuffer == InvalidBuffer) if (otherBuffer == InvalidBuffer)
{ {
/* easy case */ /* easy case */
buffer = ReadBufferBI(relation, targetBlock, bistate); buffer = ReadBufferBI(relation, targetBlock, RBM_NORMAL, bistate);
if (PageIsAllVisible(BufferGetPage(buffer))) if (PageIsAllVisible(BufferGetPage(buffer)))
visibilitymap_pin(relation, targetBlock, vmbuffer); visibilitymap_pin(relation, targetBlock, vmbuffer);
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
...@@ -479,6 +486,19 @@ loop: ...@@ -479,6 +486,19 @@ loop:
* we're done. * we're done.
*/ */
page = BufferGetPage(buffer); page = BufferGetPage(buffer);
/*
* If necessary initialize page, it'll be used soon. We could avoid
* dirtying the buffer here, and rely on the caller to do so whenever
* it puts a tuple onto the page, but there seems not much benefit in
* doing so.
*/
if (PageIsNew(page))
{
PageInit(page, BufferGetPageSize(buffer), 0);
MarkBufferDirty(buffer);
}
pageFreeSpace = PageGetHeapFreeSpace(page); pageFreeSpace = PageGetHeapFreeSpace(page);
if (len + saveFreeSpace <= pageFreeSpace) if (len + saveFreeSpace <= pageFreeSpace)
{ {
...@@ -571,42 +591,67 @@ loop: ...@@ -571,42 +591,67 @@ loop:
* it worth keeping an accurate file length in shared memory someplace, * it worth keeping an accurate file length in shared memory someplace,
* rather than relying on the kernel to do it for us? * rather than relying on the kernel to do it for us?
*/ */
buffer = ReadBufferBI(relation, P_NEW, bistate); buffer = ReadBufferBI(relation, P_NEW, RBM_ZERO_AND_LOCK, bistate);
/* /*
* We can be certain that locking the otherBuffer first is OK, since it * We need to initialize the empty new page. Double-check that it really
* must have a lower page number. * is empty (this should never happen, but if it does we don't want to
* risk wiping out valid data).
*/ */
if (otherBuffer != InvalidBuffer) page = BufferGetPage(buffer);
LockBuffer(otherBuffer, BUFFER_LOCK_EXCLUSIVE);
/* if (!PageIsNew(page))
* Now acquire lock on the new page. elog(ERROR, "page %u of relation \"%s\" should be empty but is not",
*/ BufferGetBlockNumber(buffer),
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); RelationGetRelationName(relation));
PageInit(page, BufferGetPageSize(buffer), 0);
MarkBufferDirty(buffer);
/* /*
* Release the file-extension lock; it's now OK for someone else to extend * Release the file-extension lock; it's now OK for someone else to extend
* the relation some more. Note that we cannot release this lock before * the relation some more.
* we have buffer lock on the new page, or we risk a race condition
* against vacuumlazy.c --- see comments therein.
*/ */
if (needLock) if (needLock)
UnlockRelationForExtension(relation, ExclusiveLock); UnlockRelationForExtension(relation, ExclusiveLock);
/* /*
* We need to initialize the empty new page. Double-check that it really * Lock the other buffer. It's guaranteed to be of a lower page number
* is empty (this should never happen, but if it does we don't want to * than the new page. To conform with the deadlock prevent rules, we ought
* risk wiping out valid data). * to lock otherBuffer first, but that would give other backends a chance
* to put tuples on our page. To reduce the likelihood of that, attempt to
* lock the other buffer conditionally, that's very likely to work.
* Otherwise we need to lock buffers in the correct order, and retry if
* the space has been used in the mean time.
*
* Alternatively, we could acquire the lock on otherBuffer before
* extending the relation, but that'd require holding the lock while
* performing IO, which seems worse than an unlikely retry.
*/ */
page = BufferGetPage(buffer); if (otherBuffer != InvalidBuffer)
{
Assert(otherBuffer != buffer);
if (!PageIsNew(page)) if (unlikely(!ConditionalLockBuffer(otherBuffer)))
elog(ERROR, "page %u of relation \"%s\" should be empty but is not", {
BufferGetBlockNumber(buffer), LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
RelationGetRelationName(relation)); LockBuffer(otherBuffer, BUFFER_LOCK_EXCLUSIVE);
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
PageInit(page, BufferGetPageSize(buffer), 0); /*
* Because the buffer was unlocked for a while, it's possible,
* although unlikely, that the page was filled. If so, just retry
* from start.
*/
if (len > PageGetHeapFreeSpace(page))
{
LockBuffer(otherBuffer, BUFFER_LOCK_UNLOCK);
UnlockReleaseBuffer(buffer);
goto loop;
}
}
}
if (len > PageGetHeapFreeSpace(page)) if (len > PageGetHeapFreeSpace(page))
{ {
......
...@@ -860,43 +860,46 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, ...@@ -860,43 +860,46 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
if (PageIsNew(page)) if (PageIsNew(page))
{ {
bool still_new;
/* /*
* An all-zeroes page could be left over if a backend extends the * All-zeroes pages can be left over if either a backend extends
* relation but crashes before initializing the page. Reclaim such * the relation by a single page, but crashes before the newly
* pages for use. * initialized page has been written out, or when bulk-extending
* * the relation (which creates a number of empty pages at the tail
* We have to be careful here because we could be looking at a * end of the relation, but enters them into the FSM).
* page that someone has just added to the relation and not yet
* been able to initialize (see RelationGetBufferForTuple). To
* protect against that, release the buffer lock, grab the
* relation extension lock momentarily, and re-lock the buffer. If
* the page is still uninitialized by then, it must be left over
* from a crashed backend, and we can initialize it.
* *
* We don't really need the relation lock when this is a new or * Make sure these pages are in the FSM, to ensure they can be
* temp relation, but it's probably not worth the code space to * reused. Do that by testing if there's any space recorded for
* check that, since this surely isn't a critical path. * the page. If not, enter it.
* *
* Note: the comparable code in vacuum.c need not worry because * Note we do not enter the page into the visibilitymap. That has
* it's got exclusive lock on the whole relation. * the downside that we repeatedly visit this page in subsequent
* vacuums, but otherwise we'll never not discover the space on a
* promoted standby. The harm of repeated checking ought to
* normally not be too bad - the space usually should be used at
* some point, otherwise there wouldn't be any regular vacuums.
*/ */
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
LockRelationForExtension(onerel, ExclusiveLock); /*
UnlockRelationForExtension(onerel, ExclusiveLock); * Perform checking of FSM after releasing lock, the fsm is
LockBufferForCleanup(buf); * approximate, after all.
if (PageIsNew(page)) */
still_new = PageIsNew(page);
UnlockReleaseBuffer(buf);
if (still_new)
{ {
ereport(WARNING,
(errmsg("relation \"%s\" page %u is uninitialized --- fixing",
relname, blkno)));
PageInit(page, BufferGetPageSize(buf), 0);
empty_pages++; empty_pages++;
}
freespace = PageGetHeapFreeSpace(page);
MarkBufferDirty(buf);
UnlockReleaseBuffer(buf);
if (GetRecordedFreeSpace(onerel, blkno) == 0)
{
Size freespace;
freespace = BufferGetPageSize(buf) - SizeOfPageHeaderData;
RecordPageWithFreeSpace(onerel, blkno, freespace); RecordPageWithFreeSpace(onerel, blkno, freespace);
}
}
continue; continue;
} }
...@@ -905,7 +908,10 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, ...@@ -905,7 +908,10 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
empty_pages++; empty_pages++;
freespace = PageGetHeapFreeSpace(page); freespace = PageGetHeapFreeSpace(page);
/* empty pages are always all-visible and all-frozen */ /*
* Empty pages are always all-visible and all-frozen (note that
* the same is currently not true for new pages, see above).
*/
if (!PageIsAllVisible(page)) if (!PageIsAllVisible(page))
{ {
START_CRIT_SECTION(); START_CRIT_SECTION();
...@@ -1639,12 +1645,13 @@ lazy_check_needs_freeze(Buffer buf, bool *hastup) ...@@ -1639,12 +1645,13 @@ lazy_check_needs_freeze(Buffer buf, bool *hastup)
*hastup = false; *hastup = false;
/* If we hit an uninitialized page, we want to force vacuuming it. */ /*
if (PageIsNew(page)) * New and empty pages, obviously, don't contain tuples. We could make
return true; * sure that the page is registered in the FSM, but it doesn't seem worth
* waiting for a cleanup lock just for that, especially because it's
/* Quick out for ordinary empty page. */ * likely that the pin holder will do so.
if (PageIsEmpty(page)) */
if (PageIsNew(page) || PageIsEmpty(page))
return false; return false;
maxoff = PageGetMaxOffsetNumber(page); maxoff = PageGetMaxOffsetNumber(page);
...@@ -2029,7 +2036,6 @@ count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats) ...@@ -2029,7 +2036,6 @@ count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats)
if (PageIsNew(page) || PageIsEmpty(page)) if (PageIsNew(page) || PageIsEmpty(page))
{ {
/* PageIsNew probably shouldn't happen... */
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
continue; continue;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment