commit 5c8b7bb4074ccfccb8c6e6e968e68cb03938028a
Author: Robert Haas <rhaas@postgresql.org>
Date:   Fri Nov 4 10:31:18 2016 -0400

    more me

diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c
index 58e15f3..c00d6f5 100644
--- a/src/backend/access/hash/hashovfl.c
+++ b/src/backend/access/hash/hashovfl.c
@@ -372,9 +372,10 @@ _hash_firstfreebit(uint32 map)
  *	Returns the block number of the page that followed the given page
  *	in the bucket, or InvalidBlockNumber if no following page.
  *
- *	NB: caller must not hold lock on metapage, nor on either page that's
- *	adjacent in the bucket chain except from primary bucket.  The caller had
- *	better hold cleanup lock on the primary bucket page.
+ *	NB: caller must hold a cleanup lock on the primary bucket page, so that
+ *	concurrent scans can't get confused.  caller must not hold a lock on either
+ *	page adjacent to this one in the bucket chain (except when it's the primary
+ *	bucket page). caller must not hold a lock on the metapage, either.
  */
 BlockNumber
 _hash_freeovflpage(Relation rel, Buffer ovflbuf, BlockNumber bucket_blkno,
@@ -416,41 +417,42 @@ _hash_freeovflpage(Relation rel, Buffer ovflbuf, BlockNumber bucket_blkno,
 	/*
 	 * Fix up the bucket chain.  this is a doubly-linked list, so we must fix
 	 * up the bucket chain members behind and ahead of the overflow page being
-	 * deleted.  No concurrency issues since we hold the cleanup lock on
-	 * primary bucket.  We don't need to aqcuire buffer lock to fix the
-	 * primary bucket, as we already have that lock.
+	 * deleted.  No concurrency issues since we hold a cleanup lock on primary
+	 * bucket.  We don't need to acquire a buffer lock to fix the primary
+	 * bucket, as we already have that lock.
 	 */
 	if (BlockNumberIsValid(prevblkno))
 	{
+		Buffer		prevbuf;
+		Page		prevpage;
+		HashPageOpaque prevopaque;
+
 		if (prevblkno == bucket_blkno)
-		{
-			Buffer		prevbuf = ReadBufferExtended(rel, MAIN_FORKNUM,
-													 prevblkno,
-													 RBM_NORMAL,
-													 bstrategy);
+			prevbuf = ReadBufferExtended(rel, MAIN_FORKNUM,
+										 prevblkno,
+										 RBM_NORMAL,
+										 bstrategy);
+		else
+			prevbuf = _hash_getbuf_with_strategy(rel,
+												 prevblkno,
+												 HASH_WRITE,
+										   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
+												 bstrategy);
+
+		prevpage = BufferGetPage(prevbuf);
+		prevopaque = (HashPageOpaque) PageGetSpecialPointer(prevpage);
+
+		Assert(prevopaque->hasho_bucket == bucket);
+		prevopaque->hasho_nextblkno = nextblkno;
 
-			Page		prevpage = BufferGetPage(prevbuf);
-			HashPageOpaque prevopaque = (HashPageOpaque) PageGetSpecialPointer(prevpage);
 
-			Assert(prevopaque->hasho_bucket == bucket);
-			prevopaque->hasho_nextblkno = nextblkno;
+		if (prevblkno == bucket_blkno)
+		{
 			MarkBufferDirty(prevbuf);
 			ReleaseBuffer(prevbuf);
 		}
 		else
-		{
-			Buffer		prevbuf = _hash_getbuf_with_strategy(rel,
-															 prevblkno,
-															 HASH_WRITE,
-										   LH_BUCKET_PAGE | LH_OVERFLOW_PAGE,
-															 bstrategy);
-			Page		prevpage = BufferGetPage(prevbuf);
-			HashPageOpaque prevopaque = (HashPageOpaque) PageGetSpecialPointer(prevpage);
-
-			Assert(prevopaque->hasho_bucket == bucket);
-			prevopaque->hasho_nextblkno = nextblkno;
 			_hash_wrtbuf(rel, prevbuf);
-		}
 	}
 	if (BlockNumberIsValid(nextblkno))
 	{
@@ -592,8 +594,10 @@ _hash_initbitmap(Relation rel, HashMetaPage metap, BlockNumber blkno,
  *	required that to be true on entry as well, but it's a lot easier for
  *	callers to leave empty overflow pages and let this guy clean it up.
  *
- *	Caller must hold cleanup lock on the target bucket.  This allows
- *	us to safely lock multiple pages in the bucket.
+ *	Caller must hold cleanup lock on the primary page of the target bucket
+ *	to exclude any concurrent scans, which could easily be confused into
+ *	returning the same tuple more than once or some tuples not at all by
+ *	the rearrangement we are performing here.
  *
  *	Since this function is invoked in VACUUM, we provide an access strategy
  *	parameter that controls fetches of the bucket pages.
@@ -626,7 +630,7 @@ _hash_squeezebucket(Relation rel,
 
 	/*
 	 * if there aren't any overflow pages, there's nothing to squeeze. caller
-	 * is responsible to release the lock on primary bucket page.
+	 * is responsible for releasing the lock on primary bucket page.
 	 */
 	if (!BlockNumberIsValid(wopaque->hasho_nextblkno))
 		return;
