From 573cc29588490089f5208269a41c3a77a06fadcd Mon Sep 17 00:00:00 2001 From: Alexandre Felipe 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