From 573cc29588490089f5208269a41c3a77a06fadcd Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Mon, 14 Sep 2026 09:26:36 +0100
Subject: [PATCH-v4.0 4/5] Closing BufTable and bufferdesc gap.

Between inserting and locking buffer header to set the tag and stamp the buffer
with BM_TAG_VALID there was a gap where a BufferTableLookup could succed and
pin the buffer. The obvious solution is holding a header lock during insertion
However, spin-lock should not be held during long operations.

This path split the BufTable mutations (Insert/Delete) in two parts, a variable
complexity scan, and a constant complexity commit. The scan involves chasing
pointers on a chain that could in theory has as many links as there are shared
buffers. While commiting writes to one cache line for a deletion and two cache
lines for an insertion.

The variable complexity search, BuftablePrepare(Insert|Delete) is performed
under a partition LWLock, and Buftable(Insert|Delete) is performed holding
a buffer header (spin-)lock, ensuring that pinners can't see an invalid tag
due to a race between lookup and insertion.
---
 src/backend/storage/buffer/buf_table.c     | 216 +++++++++++++--------
 src/backend/storage/buffer/bufmgr.c        |  89 +++++----
 src/include/storage/buf_internals.h        |  24 ++-
 src/test/modules/microbench/bufmap/bench.c |  17 +-
 4 files changed, 221 insertions(+), 125 deletions(-)

diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c
index 54b9a09c98b..3668eeaf2a3 100644
--- a/src/backend/storage/buffer/buf_table.c
+++ b/src/backend/storage/buffer/buf_table.c
@@ -11,11 +11,11 @@
  *	  entries[NBuffers]     - one entry per buffer, indexed by buf_id
  *
  * Each buffer slot i permanently owns entry slot i, so no freelist is needed:
- * bufmgr always removes a buffer's old mapping (BufTableDelete, called from
+ * bufmgr always removes a buffer's old mapping (BufTableUnlink, called from
  * InvalidateVictimBuffer) before inserting a new tag for that same buf_id (see
  * GetVictimBuffer / BufferAlloc in bufmgr.c).  Empty entry slots are marked by
- * tag.blockNum == P_NEW; chains are linked by int index and terminated by
- * BUF_TABLE_CHAIN_END.
+ * bucket == BUF_TABLE_CHAIN_END; chains are linked by int index and terminated
+ * by BUF_TABLE_CHAIN_END.
  *
  * num_buckets is a power of two and a multiple of NUM_BUFFER_PARTITIONS, so the
  * bucket index (hashcode % num_buckets) shares its low bits with the partition
@@ -24,10 +24,15 @@
  * fully serializes each chain -- the same guarantee the dynahash table relied
  * on.
  *
- * Insert and delete require the caller to hold exclusive BufMappingLock for
- * the tag's partition.  Lookup should be called without a lock and it takes
- * a shared partition lock only if it gets a stale node during the concurrent
- * scan.
+ * Prepare/insert/unlink require the caller to hold exclusive BufMappingLock
+ * for the tag's partition for the whole prepare + mutate sequence:
+ * BufTableScanResult.link is a pointer into the shared chain and is invalid
+ * after that lock is released.  The actual pointer swing (BufTableInsert /
+ * BufTableUnlink) is done while also holding the buffer header spinlock, so a
+ * lock-free lookup cannot pin a buffer whose identity is still being changed.
+ *
+ * Lookup is called without a lock and takes a shared partition lock only if it
+ * hits a stale node during the concurrent scan.
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -59,8 +64,8 @@ typedef struct
 /* entry for buffer lookup hashtable */
 typedef struct
 {
-	uint32 		hashcode;
-	BufferTag	tag;			/* Tag of a disk page, or P_NEW if empty */
+	uint32		hashcode;
+	BufferTag	tag;			/* Tag of a disk page */
 	int			next;			/* next entry in hash chain */
 	uint32		bucket;
 } BufferLookupEnt;
@@ -121,7 +126,7 @@ BufTableShmemRequest(void *arg)
  *
  * Shared memory is zeroed, but zero is a valid buf_id and block 0 is a valid
  * block number, so we must explicitly mark every bucket empty
- * (BUF_TABLE_CHAIN_END) and every entry empty (tag.blockNum == P_NEW).
+ * (BUF_TABLE_CHAIN_END) and every entry empty.
  */
 void
 BufTableShmemInit(void *arg)
@@ -165,17 +170,10 @@ BufTableHashCode(BufferTag *tagPtr)
 	return tag_hash(tagPtr, sizeof(BufferTag));
 }
 
-typedef enum BufTableScanAction
-{
-	BUFTABLE_SCAN_LOOKUP,
-	BUFTABLE_SCAN_INSERT,
-	BUFTABLE_SCAN_DELETE,
-} BufTableScanAction;
-
 /*
  * BufTableScan
- *		unified code to scan the hash table and perform
- *		insertion backup and deletion.
+ *		Walk one hash chain.  Does not modify the table.
+ *
  * Invariants:
  *  - entries[id] is associated with buffer id.
  *  - a chain must end with a link to BUF_TABLE_CHAIN_END
@@ -184,119 +182,169 @@ typedef enum BufTableScanAction
  *  - entries[buckets[bucket].head].bucket == bucket
  *  - entries[entries[id].next].bucket == entries[id].bucket
  *
- * Chains are sorted by hashcode.  Insert splices at the first node with a
- * greater hash (or at the end).  A node whose bucket field does not match is
- * a leftover pointer to a recycled slot; lookup retries under a shared
- * partition lock, insert/delete treat that as corruption.
+ * On a hit, result->found is the buf_id and result->link points at the
+ * predecessor's next pointer.  On a miss, found is BUF_TABLE_CHAIN_END and
+ * link is the splice point (first node with a greater hash, or the tail).
+ *
+ * A node whose bucket field does not match is a leftover pointer to a
+ * recycled slot: result->link is left NULL so lookup can retry under a
+ * shared partition lock.  Prepare insert/delete treat that as corruption.
  */
-static pg_always_inline int
+static pg_always_inline void
 BufTableScan(BufferTag *tagPtr, uint32 hashcode,
-			 BufTableScanAction action, int buf_id)
+			 BufTableScanResult *result)
 {
 	int			bucket = hashcode & (num_buckets - 1);
 	int		   *link;
-	LWLock	   *lock = NULL;
 	int			id;
 
-scan:
+	result->link = NULL;
+	result->bucket = bucket;
+	result->found = BUF_TABLE_CHAIN_END;
+
 	for (link = &buckets[bucket].head;
 		 (id = *link) != BUF_TABLE_CHAIN_END;
 		 link = &entries[id].next)
 	{
+		pg_read_barrier();
 		if (entries[id].bucket != bucket)
-			goto broken;
+			return;
 		if (entries[id].hashcode > hashcode)
 			break;
 		if (entries[id].hashcode < hashcode)
 			continue;
 		if (BufferTagsEqual(&entries[id].tag, tagPtr))
 		{
-			if (action == BUFTABLE_SCAN_DELETE)
-			{
-				*link = entries[id].next;
-				entries[id].next = BUF_TABLE_CHAIN_END;
-				entries[id].bucket = BUF_TABLE_CHAIN_END;
-			}
-			if (lock)
-				LWLockRelease(lock);
-			return id;
+			result->link = link;
+			result->found = id;
+			return;
 		}
 	}
+	result->link = link;
+}
 
-	if (action == BUFTABLE_SCAN_INSERT)
-	{
-		Assert(entries[buf_id].bucket == BUF_TABLE_CHAIN_END);
-		entries[buf_id].tag = *tagPtr;
-		entries[buf_id].hashcode = hashcode;
-		entries[buf_id].next = id;
-		entries[buf_id].bucket = bucket;
-		*link = buf_id;
-	}
-	else if (action == BUFTABLE_SCAN_DELETE)
-		elog(ERROR, "shared buffer hash table corrupted");
-
-	if (lock)
-		LWLockRelease(lock);
-	return -1;
+/*
+ * BufTableLookup
+ *		Lookup the given BufferTag; return buffer ID, or -1 if not found
+ *
+ * Fast path attempt without a lock; it might fail if a node is reused while
+ * we hold a reference to it.  In that case a shared lock is acquired and the
+ * scan is repeated.
+ *
+ * Concurrency:
+ *   Conflicts with deletion on the same bucket; concurrent insertions
+ *   linearize with lookup.  On a stale node it acquires LW_SHARED so the
+ *   caller need not hold a lock for as long as deletions hold LW_EXCLUSIVE.
+ */
+int
+BufTableLookup(BufferTag *tagPtr, uint32 hashcode)
+{
+	BufTableScanResult r;
+	LWLock	   *lock = NULL;
 
-broken:
-	if (action != BUFTABLE_SCAN_LOOKUP || lock)
+	for (;;)
 	{
+		BufTableScan(tagPtr, hashcode, &r);
+		if (r.link != NULL)
+		{
+			if (lock)
+				LWLockRelease(lock);
+			return r.found;
+		}
+
+		/* Concurrent reuse of a node we were following. */
 		if (lock)
-			LWLockRelease(lock);
-		elog(ERROR, "shared buffer hash table corrupted");
+			elog(ERROR, "shared buffer hash table corrupted");
+
+		lock = BufMappingPartitionLock(hashcode);
+		LWLockAcquire(lock, LW_SHARED);
 	}
-	lock = BufMappingPartitionLock(hashcode);
-	LWLockAcquire(lock, LW_SHARED);
-	goto scan;
 }
+
 /*
- * BufTableLookup
- *		Lookup the given BufferTag; return buffer ID, or -1 if not found
+ * BufTablePrepareInsert
+ *		Determine the splice point for a given tag without modifying the table.
  *
- * Fast path attempt without a lock, it might fail if a node is reused,
- * while we hold a reference to it. In that case the a shared lock is
- * acquired and the scan is repeated.
+ * The subsequent BufTableInsert must run before the exclusive partition lock
+ * is released, typically while also holding the victim's buffer header lock.
  *
  * Concurrency:
- *   Conflicts with deletion on the same bucket, concurrent insertions
- *   linearise with lookup. On conflict it acquires a LW_SHARED partition
- *   lock, so that a caller doesn't have to hold as long as deletions
- *   hold a LW_EXCLUSIVE partition lock.
+ *   Conflicts with same-tag insertion and with deletions on the same bucket.
+ *
+ * Returns the existing buf_id on collision, or -1 if the tag is absent.
  */
 int
-BufTableLookup(BufferTag *tagPtr, uint32 hashcode)
+BufTablePrepareInsert(BufferTag *tagPtr, uint32 hashcode,
+					  BufTableScanResult *result)
 {
-	return BufTableScan(tagPtr, hashcode, BUFTABLE_SCAN_LOOKUP, BUF_TABLE_CHAIN_END);
+	BufTableScan(tagPtr, hashcode, result);
+	if (result->link == NULL)
+		elog(ERROR, "shared buffer hash table corrupted");
+	return result->found;
 }
 
 /*
  * BufTableInsert
- *   Insert a hashtable entry for given tag and buffer ID,
- *   unless an entry already exists for that tag
+ *		Splice buf_id into the chain at the prepared location.
+ *
+ * Must be called only after a miss from BufTablePrepareInsert (found < 0).
+ * Writes the entry fields, then publishes *link so lock-free lookup can see
+ * the node.  Caller should hold the buffer header spinlock so PinBuffer waits
+ * until BufferDesc.tag / BM_TAG_VALID are installed.
+ */
+void
+BufTableInsert(BufTableScanResult *result, BufferTag *tagPtr,
+			   uint32 hashcode, int buf_id)
+{
+	Assert(buf_id >= 0 && buf_id < NBuffers);
+	Assert(result->link != NULL);
+	Assert(result->found == BUF_TABLE_CHAIN_END);
+	Assert(entries[buf_id].bucket == (uint32) BUF_TABLE_CHAIN_END);
+
+	entries[buf_id].tag = *tagPtr;
+	entries[buf_id].hashcode = hashcode;
+	entries[buf_id].next = *result->link;
+	entries[buf_id].bucket = result->bucket;
+	pg_write_barrier();
+	*result->link = buf_id;
+}
+
+/*
+ * BufTablePrepareDelete
+ *		Find the predecessor link for an existing entry without unlinking it.
  *
- * Returns -1 on successful insertion.  If a conflicting entry exists
- * already, returns the buffer ID in that entry.
+ * The subsequent BufTableUnlink must run before the exclusive partition lock
+ * is released, typically while also holding the buffer header spinlock.
  *
- * Concurrency:
- *   Conflicts with same tag insertion and deletions on same bucket.
+ * Returns the matching buf_id, or -1 if the tag is not in the table.
  */
 int
-BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id)
+BufTablePrepareDelete(BufferTag *tagPtr, uint32 hashcode,
+					  BufTableScanResult *result)
 {
-	Assert(buf_id >= 0 && buf_id < NBuffers);
-	return BufTableScan(tagPtr, hashcode, BUFTABLE_SCAN_INSERT, buf_id);
+	BufTableScan(tagPtr, hashcode, result);
+	if (result->link == NULL)
+		elog(ERROR, "shared buffer hash table corrupted");
+	return result->found;
 }
 
 /*
- * BufTableDelete
- *		Delete the hashtable entry for given tag (which must exist)
+ * BufTableUnlink
+ *		Remove the prepared entry from its hash chain.
  *
  * Concurrency:
- * 		Conflicts with deletion or insertion on the same bucket.
+ *		Conflicts with deletion or insertion on the same bucket.
  */
 void
-BufTableDelete(BufferTag *tagPtr, uint32 hashcode)
+BufTableUnlink(BufTableScanResult *result)
 {
-	BufTableScan(tagPtr, hashcode, BUFTABLE_SCAN_DELETE, BUF_TABLE_CHAIN_END);
+	int			id = result->found;
+
+	Assert(result->link != NULL);
+	Assert(id >= 0 && id < NBuffers);
+
+	*result->link = entries[id].next;
+	pg_write_barrier();
+	entries[id].next = BUF_TABLE_CHAIN_END;
+	entries[id].bucket = BUF_TABLE_CHAIN_END;
 }
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f81411803f8..46ec2f6f091 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2207,6 +2207,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	BufferDesc *victim_buf_hdr;
 	uint64		victim_buf_state;
 	uint64		set_bits = 0;
+	BufTableScanResult mapping;
 
 	/* Make sure we will have room to remember the buffer pin */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
@@ -2284,7 +2285,7 @@ retry_lookup:
 	 * victim buffer we acquired and use the already inserted one.
 	 */
 	LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
-	existing_buf_id = BufTableInsert(&newTag, newHash, victim_buf_hdr->buf_id);
+	existing_buf_id = BufTablePrepareInsert(&newTag, newHash, &mapping);
 	if (existing_buf_id >= 0)
 	{
 		BufferDesc *existing_buf_hdr;
@@ -2307,6 +2308,7 @@ retry_lookup:
 		existing_buf_hdr = GetBufferDescriptor(existing_buf_id);
 
 		valid = PinBuffer(existing_buf_hdr, strategy, false);
+		Assert(BufferTagsEqual(&newTag, &existing_buf_hdr->tag));
 
 		/* Can release the mapping lock as soon as we've pinned it */
 		LWLockRelease(newPartitionLock);
@@ -2325,9 +2327,10 @@ retry_lookup:
 
 		return existing_buf_hdr;
 	}
-
 	/*
 	 * Need to lock the buffer header too in order to change its tag.
+	 * Publish the mapping while the header is still locked so a lock-free
+	 * lookup cannot pin until tag is updated and BM_TAG_VALID is set.
 	 */
 	victim_buf_state = LockBufHdr(victim_buf_hdr);
 
@@ -2347,6 +2350,8 @@ retry_lookup:
 	if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
 		set_bits |= BM_PERMANENT;
 
+	BufTableInsert(&mapping, &newTag, newHash, victim_buf_hdr->buf_id);
+
 	UnlockBufHdrExt(victim_buf_hdr, victim_buf_state,
 					set_bits, 0, 0);
 
@@ -2384,7 +2389,7 @@ InvalidateBuffer(BufferDesc *buf)
 	LWLock	   *oldPartitionLock;	/* buffer partition lock for it */
 	uint32		oldFlags;
 	uint64		buf_state;
-
+	BufTableScanResult mapping;
 	/* Save the original buffer tag before dropping the spinlock */
 	oldTag = buf->tag;
 
@@ -2405,6 +2410,7 @@ retry:
 	 * association.
 	 */
 	LWLockAcquire(oldPartitionLock, LW_EXCLUSIVE);
+	BufTablePrepareDelete(&oldTag, oldHash, &mapping);
 
 	/* Re-lock the buffer header */
 	buf_state = LockBufHdr(buf);
@@ -2418,15 +2424,15 @@ retry:
 	}
 
 	/*
-	 * We assume the reason for it to be pinned is that either we were
-	 * asynchronously reading the page in before erroring out or someone else
-	 * is flushing the page out.  Wait for the IO to finish.  (This could be
-	 * an infinite loop if the refcount is messed up... it would be nice to
-	 * time out after awhile, but there seems no way to be sure how many loops
-	 * may be needed.  Note that if the other guy has pinned the buffer but
-	 * not yet done StartBufferIO, WaitIO will fall through and we'll
-	 * effectively be busy-looping here.)
-	 */
+		* We assume the reason for it to be pinned is that either we were
+		* asynchronously reading the page in before erroring out or someone else
+		* is flushing the page out.  Wait for the IO to finish.  (This could be
+		* an infinite loop if the refcount is messed up... it would be nice to
+		* time out after awhile, but there seems no way to be sure how many loops
+		* may be needed.  Note that if the other guy has pinned the buffer but
+		* not yet done StartBufferIO, WaitIO will fall through and we'll
+		* effectively be busy-looping here.)
+		*/
 	if (BUF_STATE_GET_REFCOUNT(buf_state) != 0)
 	{
 		UnlockBufHdr(buf);
@@ -2439,16 +2445,24 @@ retry:
 	}
 
 	/*
-	 * An invalidated buffer should not have any backends waiting to lock the
-	 * buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set.
-	 */
+		* An invalidated buffer should not have any backends waiting to lock the
+		* buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set.
+		*/
 	Assert(!(buf_state & BM_LOCK_WAKE_IN_PROGRESS));
 
 	/*
-	 * Clear out the buffer's tag and flags.  We must do this to ensure that
-	 * linear scans of the buffer array don't think the buffer is valid.
-	 */
+		* Clear out the buffer's tag and flags.  We must do this to ensure that
+		* linear scans of the buffer array don't think the buffer is valid.
+		*/
 	oldFlags = buf_state & BUF_FLAG_MASK;
+
+	if (oldFlags & BM_TAG_VALID)
+	{
+		if (mapping.found != buf->buf_id)
+			elog(ERROR, "shared buffer hash table corrupted");
+		BufTableUnlink(&mapping);
+	}
+
 	ClearBufferTag(&buf->tag);
 
 	UnlockBufHdrExt(buf, buf_state,
@@ -2456,12 +2470,6 @@ retry:
 					BUF_FLAG_MASK | BUF_USAGECOUNT_MASK,
 					0);
 
-	/*
-	 * Remove the buffer from the lookup hashtable, if it was in there.
-	 */
-	if (oldFlags & BM_TAG_VALID)
-		BufTableDelete(&oldTag, oldHash);
-
 	/*
 	 * Done with mapping lock.
 	 */
@@ -2484,6 +2492,7 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 	uint32		hash;
 	LWLock	   *partition_lock;
 	BufferTag	tag;
+	BufTableScanResult mapping;
 
 	Assert(GetPrivateRefCount(BufferDescriptorGetBuffer(buf_hdr)) == 1);
 
@@ -2495,21 +2504,23 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 
 	LWLockAcquire(partition_lock, LW_EXCLUSIVE);
 
+	BufTablePrepareDelete(&tag, hash, &mapping);
+
 	/* lock the buffer header */
 	buf_state = LockBufHdr(buf_hdr);
 
 	/*
-	 * We have the buffer pinned nobody else should have been able to unset
-	 * this concurrently.
-	 */
+		* We have the buffer pinned nobody else should have been able to unset
+		* this concurrently.
+		*/
 	Assert(buf_state & BM_TAG_VALID);
 	Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);
 	Assert(BufferTagsEqual(&buf_hdr->tag, &tag));
 
 	/*
-	 * If somebody else pinned the buffer since, or even worse, dirtied it,
-	 * give up on this buffer: It's clearly in use.
-	 */
+		* If somebody else pinned the buffer since, or even worse, dirtied it,
+		* give up on this buffer: It's clearly in use.
+		*/
 	if (BUF_STATE_GET_REFCOUNT(buf_state) != 1 || (buf_state & BM_DIRTY))
 	{
 		Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);
@@ -2521,11 +2532,15 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 	}
 
 	/*
-	 * An invalidated buffer should not have any backends waiting to lock the
-	 * buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set.
-	 */
+		* An invalidated buffer should not have any backends waiting to lock the
+		* buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set.
+		*/
 	Assert(!(buf_state & BM_LOCK_WAKE_IN_PROGRESS));
 
+	if (mapping.found != buf_hdr->buf_id)
+		elog(ERROR, "shared buffer hash table corrupted");
+	BufTableUnlink(&mapping);
+
 	/*
 	 * Clear out the buffer's tag and flags and usagecount.  This is not
 	 * strictly required, as BM_TAG_VALID/BM_VALID needs to be checked before
@@ -2541,9 +2556,6 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr)
 
 	Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);
 
-	/* finally delete buffer from the buffer mapping table */
-	BufTableDelete(&tag, hash);
-
 	LWLockRelease(partition_lock);
 
 	buf_state = pg_atomic_read_u64(&buf_hdr->state);
@@ -2927,6 +2939,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 		uint32		hash;
 		LWLock	   *partition_lock;
 		int			existing_id;
+		BufTableScanResult mapping;
 
 		/* in case we need to pin an existing buffer below */
 		ResourceOwnerEnlarge(CurrentResourceOwner);
@@ -2939,7 +2952,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 
 		LWLockAcquire(partition_lock, LW_EXCLUSIVE);
 
-		existing_id = BufTableInsert(&tag, hash, victim_buf_hdr->buf_id);
+		existing_id = BufTablePrepareInsert(&tag, hash, &mapping);
 
 		/*
 		 * We get here only in the corner case where we are trying to extend
@@ -3016,6 +3029,8 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 			if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM)
 				set_bits |= BM_PERMANENT;
 
+			BufTableInsert(&mapping, &tag, hash, victim_buf_hdr->buf_id);
+
 			UnlockBufHdrExt(victim_buf_hdr, buf_state,
 							set_bits, 0,
 							0);
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index e4ff5619b79..331e2d808d3 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -167,6 +167,21 @@ typedef struct buftag
 	BlockNumber blockNum;		/* blknum relative to begin of reln */
 } BufferTag;
 
+/*
+ * Result of a mapping-table walk, used to finish an insert or delete.
+ *
+ * link points at the predecessor's next pointer (or the bucket head) and is
+ * only valid while the caller holds exclusive BufMappingLock for this tag's
+ * partition.  found is the matching buf_id, or -1 if the tag is absent.
+ */
+typedef struct BufTableScanResult
+{
+	int		   *link;
+	int			found;
+	int			bucket;
+} BufTableScanResult;
+
+
 static inline RelFileNumber
 BufTagGetRelNumber(const BufferTag *tag)
 {
@@ -597,8 +612,13 @@ extern void StrategyNotifyBgWriter(int bgwprocno);
 /* buf_table.c */
 extern uint32 BufTableHashCode(BufferTag *tagPtr);
 extern int	BufTableLookup(BufferTag *tagPtr, uint32 hashcode);
-extern int	BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id);
-extern void BufTableDelete(BufferTag *tagPtr, uint32 hashcode);
+extern int	BufTablePrepareInsert(BufferTag *tagPtr, uint32 hashcode,
+								  BufTableScanResult *result);
+extern void BufTableInsert(BufTableScanResult *result, BufferTag *tagPtr,
+						   uint32 hashcode, int buf_id);
+extern int	BufTablePrepareDelete(BufferTag *tagPtr, uint32 hashcode,
+								  BufTableScanResult *result);
+extern void BufTableUnlink(BufTableScanResult *result);
 
 /* localbuf.c */
 extern bool PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount);
diff --git a/src/test/modules/microbench/bufmap/bench.c b/src/test/modules/microbench/bufmap/bench.c
index d3f508b0879..01f68205b53 100644
--- a/src/test/modules/microbench/bufmap/bench.c
+++ b/src/test/modules/microbench/bufmap/bench.c
@@ -115,7 +115,15 @@ run_bufmap_bench(int proc_id, int n_parallel, int rounds, int iterations,
 				hash = BufTableHashCode(tag);
 				lock = BufMappingPartitionLock(hash);
 				group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1;
-				sink += BufTableInsert(tag, hash, (Buffer)bufids[i]);
+				{
+					BufTableScanResult mapping;
+					int			found;
+
+					found = BufTablePrepareInsert(tag, hash, &mapping);
+					if (found < 0)
+						BufTableInsert(&mapping, tag, hash, (int) bufids[i]);
+					sink += found;
+				}
 				LWLockRelease(lock);
 			}
 		END_GROUPED_TIMING;
@@ -157,7 +165,12 @@ run_bufmap_bench(int proc_id, int n_parallel, int rounds, int iterations,
 				hash = BufTableHashCode(tag);
 				lock = BufMappingPartitionLock(hash);
 				group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1;
-				BufTableDelete(tag, hash);
+				{
+					BufTableScanResult mapping;
+
+					if (BufTablePrepareDelete(tag, hash, &mapping) >= 0)
+						BufTableUnlink(&mapping);
+				}
 				LWLockRelease(lock);
 			}
 		END_GROUPED_TIMING;
-- 
2.53.0

