Skip to content

Commit f155466

Browse files
committed
Fix bogus concurrent use of _hash_getnewbuf() in bucket split code.
_hash_splitbucket() obtained the base page of the new bucket by calling _hash_getnewbuf(), but it held no exclusive lock that would prevent some other process from calling _hash_getnewbuf() at the same time. This is contrary to _hash_getnewbuf()'s API spec and could in fact cause failures. In practice, we must only call that function while holding write lock on the hash index's metapage. An additional problem was that we'd already modified the metapage's bucket mapping data, meaning that failure to extend the index would leave us with a corrupt index. Fix both issues by moving the _hash_getnewbuf() call to just before we modify the metapage in _hash_expandtable(). Unfortunately there's still a large problem here, which is that we could also incur ENOSPC while trying to get an overflow page for the new bucket. That would leave the index corrupt in a more subtle way, namely that some index tuples that should be in the new bucket might still be in the old one. Fixing that seems substantially more difficult; even preallocating as many pages as we could possibly need wouldn't entirely guarantee that the bucket split would complete successfully. So for today let's just deal with the base case. Per report from Antonin Houska. Back-patch to all active branches.
1 parent d12afe1 commit f155466

File tree

1 file changed

+26
-4
lines changed

1 file changed

+26
-4
lines changed

src/backend/access/hash/hashpage.c

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
static bool _hash_alloc_buckets(Relation rel, BlockNumber firstblock,
3838
uint32 nblocks);
3939
static void _hash_splitbucket(Relation rel, Buffer metabuf,
40+
Buffer nbuf,
4041
Bucket obucket, Bucket nbucket,
4142
BlockNumber start_oblkno,
4243
BlockNumber start_nblkno,
@@ -176,7 +177,9 @@ _hash_getinitbuf(Relation rel, BlockNumber blkno)
176177
* EOF but before updating the metapage to reflect the added page.)
177178
*
178179
* It is caller's responsibility to ensure that only one process can
179-
* extend the index at a time.
180+
* extend the index at a time. In practice, this function is called
181+
* only while holding write lock on the metapage, because adding a page
182+
* is always associated with an update of metapage data.
180183
*/
181184
Buffer
182185
_hash_getnewbuf(Relation rel, BlockNumber blkno, ForkNumber forkNum)
@@ -503,6 +506,7 @@ _hash_expandtable(Relation rel, Buffer metabuf)
503506
uint32 spare_ndx;
504507
BlockNumber start_oblkno;
505508
BlockNumber start_nblkno;
509+
Buffer buf_nblkno;
506510
uint32 maxbucket;
507511
uint32 highmask;
508512
uint32 lowmask;
@@ -615,6 +619,13 @@ _hash_expandtable(Relation rel, Buffer metabuf)
615619
}
616620
}
617621

622+
/*
623+
* Physically allocate the new bucket's primary page. We want to do this
624+
* before changing the metapage's mapping info, in case we can't get the
625+
* disk space.
626+
*/
627+
buf_nblkno = _hash_getnewbuf(rel, start_nblkno, MAIN_FORKNUM);
628+
618629
/*
619630
* Okay to proceed with split. Update the metapage bucket mapping info.
620631
*
@@ -668,7 +679,8 @@ _hash_expandtable(Relation rel, Buffer metabuf)
668679
_hash_droplock(rel, 0, HASH_EXCLUSIVE);
669680

670681
/* Relocate records to the new bucket */
671-
_hash_splitbucket(rel, metabuf, old_bucket, new_bucket,
682+
_hash_splitbucket(rel, metabuf, buf_nblkno,
683+
old_bucket, new_bucket,
672684
start_oblkno, start_nblkno,
673685
maxbucket, highmask, lowmask);
674686

@@ -751,10 +763,16 @@ _hash_alloc_buckets(Relation rel, BlockNumber firstblock, uint32 nblocks)
751763
* The caller must hold a pin, but no lock, on the metapage buffer.
752764
* The buffer is returned in the same state. (The metapage is only
753765
* touched if it becomes necessary to add or remove overflow pages.)
766+
*
767+
* In addition, the caller must have created the new bucket's base page,
768+
* which is passed in buffer nbuf, pinned and write-locked. The lock
769+
* and pin are released here. (The API is set up this way because we must
770+
* do _hash_getnewbuf() before releasing the metapage write lock.)
754771
*/
755772
static void
756773
_hash_splitbucket(Relation rel,
757774
Buffer metabuf,
775+
Buffer nbuf,
758776
Bucket obucket,
759777
Bucket nbucket,
760778
BlockNumber start_oblkno,
@@ -766,7 +784,6 @@ _hash_splitbucket(Relation rel,
766784
BlockNumber oblkno;
767785
BlockNumber nblkno;
768786
Buffer obuf;
769-
Buffer nbuf;
770787
Page opage;
771788
Page npage;
772789
HashPageOpaque oopaque;
@@ -783,7 +800,7 @@ _hash_splitbucket(Relation rel,
783800
oopaque = (HashPageOpaque) PageGetSpecialPointer(opage);
784801

785802
nblkno = start_nblkno;
786-
nbuf = _hash_getnewbuf(rel, nblkno, MAIN_FORKNUM);
803+
Assert(nblkno == BufferGetBlockNumber(nbuf));
787804
npage = BufferGetPage(nbuf);
788805

789806
/* initialize the new bucket's primary page */
@@ -832,6 +849,11 @@ _hash_splitbucket(Relation rel,
832849
* insert the tuple into the new bucket. if it doesn't fit on
833850
* the current page in the new bucket, we must allocate a new
834851
* overflow page and place the tuple on that page instead.
852+
*
853+
* XXX we have a problem here if we fail to get space for a
854+
* new overflow page: we'll error out leaving the bucket split
855+
* only partially complete, meaning the index is corrupt,
856+
* since searches may fail to find entries they should find.
835857
*/
836858
itemsz = IndexTupleDSize(*itup);
837859
itemsz = MAXALIGN(itemsz);

0 commit comments

Comments
 (0)