This is an automated email from the ASF dual-hosted git repository.
wchevreuil pushed a commit to branch branch-3
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-3 by this push:
new 0f9816e4408 HBASE-30285 BucketCache shutdown/disable does not release
backingMap-owned BucketEntry references (#8469)
0f9816e4408 is described below
commit 0f9816e44081c074c877b86c5630727d7f036e88
Author: Minwoo Kang <[email protected]>
AuthorDate: Fri Jul 31 22:14:23 2026 +0900
HBASE-30285 BucketCache shutdown/disable does not release backingMap-owned
BucketEntry references (#8469)
Signed-off-by: Wellington Chevreuil <[email protected]>
---
.../hadoop/hbase/io/hfile/bucket/BucketCache.java | 221 +++++++++----
.../hbase/io/hfile/bucket/BucketProtoUtils.java | 8 +
.../hfile/TestBlockEvictionOnRegionMovement.java | 29 +-
.../hbase/io/hfile/bucket/TestBucketCache.java | 38 ++-
.../io/hfile/bucket/TestBucketCacheRefCnt.java | 349 +++++++++++++++++++++
5 files changed, 560 insertions(+), 85 deletions(-)
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java
index 19cf2af56ee..e0d78a7dace 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java
@@ -92,6 +92,7 @@ import org.apache.hadoop.hbase.util.IdReadWriteLockStrongRef;
import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool;
import
org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType;
import org.apache.hadoop.hbase.util.Pair;
+import org.apache.hadoop.hbase.util.Threads;
import org.apache.hadoop.util.StringUtils;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
@@ -206,6 +207,12 @@ public class BucketCache implements BlockCache, HeapSize {
*/
private volatile CacheState cacheState;
+ /** The single cleanup thread shared by disable and explicit shutdown calls.
*/
+ private volatile Thread cacheCleanupThread;
+
+ /** The thread restoring the persistent cache index during initialization. */
+ private volatile Thread persistenceRetrieverThread;
+
/**
* A list of writer queues. We have a queue per {@link WriterThread} we have
running. In other
* words, the work adding blocks to the BucketCache is divided up amongst
the running
@@ -416,12 +423,17 @@ public class BucketCache implements BlockCache, HeapSize {
LOG.error("Exception during Bucket Allocation", allocatorException);
}
} finally {
- this.cacheState = CacheState.ENABLED;
- startWriterThreads();
+ synchronized (BucketCache.this) {
+ if (cacheState == CacheState.INITIALIZING) {
+ cacheState = CacheState.ENABLED;
+ startWriterThreads();
+ }
+ }
}
};
- Thread t = new Thread(persistentCacheRetriever);
- t.start();
+ persistenceRetrieverThread = new Thread(persistentCacheRetriever,
+ "BucketCachePersistenceRetriever-" + System.identityHashCode(this));
+ persistenceRetrieverThread.start();
}
private void sanityCheckConfigs() {
@@ -643,21 +655,24 @@ public class BucketCache implements BlockCache, HeapSize {
* the passed key doesn't relate to a reference.
*/
public BucketEntry getBlockForReference(BlockCacheKey key) {
- BucketEntry foundEntry = null;
- String referredFileName = null;
- if (StoreFileInfo.isReference(key.getHfileName())) {
- referredFileName =
StoreFileInfo.getReferredToRegionAndFile(key.getHfileName()).getSecond();
- }
- if (referredFileName != null) {
- // Since we just need this key for a lookup, it's enough to use only
name and offset
- BlockCacheKey convertedCacheKey = new BlockCacheKey(referredFileName,
key.getOffset());
- foundEntry = backingMap.get(convertedCacheKey);
+ BlockCacheKey referredKey = getBlockKeyForReference(key);
+ BucketEntry foundEntry = referredKey != null ? backingMap.get(referredKey)
: null;
+ if (referredKey != null) {
LOG.debug("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}",
key.getHfileName(),
- convertedCacheKey, foundEntry);
+ referredKey, foundEntry);
}
return foundEntry;
}
+ private BlockCacheKey getBlockKeyForReference(BlockCacheKey key) {
+ if (!StoreFileInfo.isReference(key.getHfileName())) {
+ return null;
+ }
+ String referredFileName =
+ StoreFileInfo.getReferredToRegionAndFile(key.getHfileName()).getSecond();
+ return referredFileName != null ? new BlockCacheKey(referredFileName,
key.getOffset()) : null;
+ }
+
/**
* Get the buffer of the block with the specified key.
* @param key block's cache key
@@ -681,23 +696,28 @@ public class BucketCache implements BlockCache, HeapSize {
re.access(accessCount.incrementAndGet());
return re.getData();
}
- BucketEntry bucketEntry = backingMap.get(key);
+ BlockCacheKey backingMapLookupKey = key;
+ BucketEntry bucketEntry = backingMap.get(backingMapLookupKey);
LOG.debug("bucket entry for key {}: {}", key,
bucketEntry == null ? null : bucketEntry.offset());
if (bucketEntry == null) {
- bucketEntry = getBlockForReference(key);
+ backingMapLookupKey = getBlockKeyForReference(key);
+ if (backingMapLookupKey != null) {
+ bucketEntry = backingMap.get(backingMapLookupKey);
+ LOG.debug("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}",
key.getHfileName(),
+ backingMapLookupKey, bucketEntry);
+ }
}
if (bucketEntry != null) {
long start = System.nanoTime();
ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset());
+ boolean inconsistentEntry = false;
try {
lock.readLock().lock();
// We can not read here even if backingMap does contain the given key
because its offset
// maybe changed. If we lock BlockCacheKey instead of offset, then we
can only check
// existence here.
- if (
- bucketEntry.equals(backingMap.get(key)) ||
bucketEntry.equals(getBlockForReference(key))
- ) {
+ if (bucketEntry.equals(backingMap.get(backingMapLookupKey))) {
// Read the block from IOEngine based on the bucketEntry's offset
and length, NOTICE: the
// block will use the refCnt of bucketEntry, which means if two
HFileBlock mapping to
// the same BucketEntry, then all of the three will share the same
refCnt.
@@ -719,11 +739,9 @@ public class BucketCache implements BlockCache, HeapSize {
return cachedBlock;
}
} catch (HBaseIOException hioex) {
- // When using file io engine persistent cache,
- // the cache map state might differ from the actual cache. If we reach
this block,
- // we should remove the cache key entry from the backing map
- backingMap.remove(key);
- fileNotFullyCached(key, bucketEntry);
+ // FileIOEngine throws this when its cached time differs from the
persisted index. A plain
+ // IOException still follows the configured tolerance policy below.
+ inconsistentEntry = true;
LOG.debug("Failed to fetch block for cache key: {}.", key, hioex);
} catch (IOException ioex) {
LOG.error("Failed reading block " + key + " from bucket cache", ioex);
@@ -731,6 +749,9 @@ public class BucketCache implements BlockCache, HeapSize {
} finally {
lock.readLock().unlock();
}
+ if (inconsistentEntry) {
+ evictInconsistentEntry(backingMapLookupKey, bucketEntry);
+ }
}
if (!repeat && updateCacheMetrics) {
cacheStats.miss(caching, key.isPrimary(), key.getBlockType());
@@ -738,6 +759,19 @@ public class BucketCache implements BlockCache, HeapSize {
return null;
}
+ private void evictInconsistentEntry(BlockCacheKey lookupKey, BucketEntry
bucketEntry) {
+ BlockCacheKey storedKey = blocksByHFile.ceiling(lookupKey);
+ if (storedKey == null || !storedKey.equals(lookupKey)) {
+ return;
+ }
+ bucketEntry.withWriteLock(offsetLock, () -> {
+ if (backingMap.remove(storedKey, bucketEntry)) {
+ blockEvicted(storedKey, bucketEntry, true, false);
+ }
+ return null;
+ });
+ }
+
/**
* This method is invoked after the bucketEntry is removed from {@link
BucketCache#backingMap}
*/
@@ -1350,7 +1384,6 @@ public class BucketCache implements BlockCache, HeapSize {
*/
protected void putIntoBackingMap(BlockCacheKey key, BucketEntry bucketEntry)
{
BucketEntry previousEntry = backingMap.put(key, bucketEntry);
- blocksByHFile.add(key);
updateRegionCachedSize(key, bucketEntry.getLength());
if (previousEntry != null && previousEntry != bucketEntry) {
previousEntry.withWriteLock(offsetLock, () -> {
@@ -1358,6 +1391,12 @@ public class BucketCache implements BlockCache, HeapSize
{
return null;
});
}
+ bucketEntry.withWriteLock(offsetLock, () -> {
+ if (backingMap.get(key) == bucketEntry) {
+ blocksByHFile.add(key);
+ }
+ return null;
+ });
}
/**
@@ -1551,6 +1590,12 @@ public class BucketCache implements BlockCache, HeapSize
{
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value =
"OBL_UNSATISFIED_OBLIGATION",
justification = "false positive, try-with-resources ensures close is
called.")
void persistToFile() throws IOException {
+ persistToFile(entry -> {
+ });
+ }
+
+ private void persistToFile(Consumer<Map.Entry<BlockCacheKey, BucketEntry>>
entryCopiedAction)
+ throws IOException {
LOG.debug("Thread {} started persisting bucket cache to file",
Thread.currentThread().getName());
if (!isCachePersistent()) {
@@ -1560,7 +1605,7 @@ public class BucketCache implements BlockCache, HeapSize {
try (FileOutputStream fos = new FileOutputStream(tempPersistencePath,
false)) {
LOG.debug("Persist in new chunked persistence format.");
- persistChunkedBackingMap(fos);
+ persistChunkedBackingMap(fos, entryCopiedAction);
LOG.debug(
"PersistToFile: after persisting backing map size: {},
fullycachedFiles size: {},"
@@ -1740,13 +1785,14 @@ public class BucketCache implements BlockCache,
HeapSize {
verifyCapacityAndClasses(proto.getCacheCapacity(), proto.getIoClass(),
proto.getMapClass());
}
- private void persistChunkedBackingMap(FileOutputStream fos) throws
IOException {
+ private void persistChunkedBackingMap(FileOutputStream fos,
+ Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) throws
IOException {
LOG.debug(
"persistToFile: before persisting backing map size: {}, "
+ "fullycachedFiles size: {}, chunkSize: {}",
backingMap.size(), fullyCachedFiles.size(), persistenceChunkSize);
- BucketProtoUtils.serializeAsPB(this, fos, persistenceChunkSize);
+ BucketProtoUtils.serializeAsPB(this, fos, persistenceChunkSize,
entryCopiedAction);
LOG.debug(
"persistToFile: after persisting backing map size: {}, " +
"fullycachedFiles size: {}",
@@ -1807,59 +1853,106 @@ public class BucketCache implements BlockCache,
HeapSize {
/**
* Used to shut down the cache -or- turn it off in the case of something
broken.
+ * @return whether explicit shutdown should wait for cleanup
*/
- private void disableCache() {
- if (!isCacheEnabled()) {
- return;
+ private synchronized boolean disableCache() {
+ if (cacheState == CacheState.DISABLED) {
+ return false;
}
+ boolean waitForCleanup = cacheState == CacheState.ENABLED &&
isCachePersistent();
LOG.info("Disabling cache");
cacheState = CacheState.DISABLED;
- ioEngine.shutdown();
this.scheduleThreadPool.shutdown();
- for (int i = 0; i < writerThreads.length; ++i)
- writerThreads[i].interrupt();
- this.ramCache.clear();
- if (!ioEngine.isPersistent() || persistencePath == null) {
- // If persistent ioengine and a path, we will serialize out the
backingMap.
- this.backingMap.clear();
- this.blocksByHFile.clear();
- this.fullyCachedFiles.clear();
- this.regionCachedSize.clear();
+ for (WriterThread writerThread : writerThreads) {
+ writerThread.interrupt();
}
+ // Closing the IO engine helps unblock an in-flight writer before the
cleanup thread joins it.
+ // FileIOEngine can reopen a channel, so cleanup closes the engine again
after writers stop.
+ ioEngine.shutdown();
if (cacheStats.getMetricsRollerScheduler() != null) {
cacheStats.getMetricsRollerScheduler().shutdownNow();
}
+ cacheCleanupThread = Threads.setDaemonThreadRunning(new
Thread(this::cleanupCache),
+ "BucketCacheCleanup-" + System.identityHashCode(this),
Threads.LOGGING_EXCEPTION_HANDLER);
+ return waitForCleanup;
}
- private void join() throws InterruptedException {
- for (int i = 0; i < writerThreads.length; ++i)
- writerThreads[i].join();
- }
-
- @Override
- public void shutdown() {
- if (isCacheEnabled()) {
- disableCache();
- LOG.info("Shutdown bucket cache: IO persistent=" +
ioEngine.isPersistent()
- + "; path to write=" + persistencePath);
- if (ioEngine.isPersistent() && persistencePath != null) {
+ private void cleanupCache() {
+ try {
+ Threads.shutdown(persistenceRetrieverThread);
+ for (WriterThread writerThread : writerThreads) {
+ Threads.shutdown(writerThread);
+ }
+ for (BlockingQueue<RAMQueueEntry> writerQueue : writerQueues) {
+ writerQueue.clear();
+ }
+ ramCache.clear();
+ if (cachePersister != null) {
+ LOG.info("Shutting down cache persister thread.");
+ cachePersister.shutdown();
+ Threads.shutdown(cachePersister);
+ }
+ if (isCachePersistent()) {
try {
- join();
- if (cachePersister != null) {
- LOG.info("Shutting down cache persister thread.");
- cachePersister.shutdown();
- while (cachePersister.isAlive()) {
- Thread.sleep(10);
- }
- }
- persistToFile();
+ // The serializer already visits every entry. Release owner
references in the same pass.
+ persistToFile(this::cleanupBackingMapEntry);
} catch (IOException ex) {
LOG.error("Unable to persist data on exit: " + ex.toString(), ex);
- } catch (InterruptedException e) {
- LOG.warn("Failed to persist data on exit", e);
}
}
+ } finally {
+ try {
+ cleanupCacheIndex();
+ } finally {
+ ioEngine.shutdown();
+ }
+ }
+ }
+
+ private void cleanupCacheIndex() {
+ // A successful persistent cleanup emptied the map during serialization.
Avoid creating a
+ // second iterator over a large ConcurrentHashMap in that case.
+ if (!backingMap.isEmpty()) {
+ for (Map.Entry<BlockCacheKey, BucketEntry> entry :
backingMap.entrySet()) {
+ cleanupBackingMapEntry(entry);
+ }
+ }
+ blocksByHFile.clear();
+ fullyCachedFiles.clear();
+ regionCachedSize.clear();
+ }
+
+ private void cleanupBackingMapEntry(Map.Entry<BlockCacheKey, BucketEntry>
entry) {
+ BlockCacheKey cacheKey = entry.getKey();
+ BucketEntry bucketEntry = entry.getValue();
+ bucketEntry.withWriteLock(offsetLock, () -> {
+ if (backingMap.remove(cacheKey, bucketEntry)) {
+ bucketEntry.markAsEvicted();
+ }
+ return null;
+ });
+ }
+
+ private void waitForCacheCleanup() throws InterruptedException {
+ Thread cleanupThread = cacheCleanupThread;
+ if (cleanupThread == null || cleanupThread == Thread.currentThread()) {
+ return;
+ }
+ cleanupThread.join();
+ }
+
+ @Override
+ public void shutdown() {
+ if (disableCache()) {
+ try {
+ waitForCacheCleanup();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOG.warn("Interrupted while waiting for bucket cache cleanup", e);
+ }
}
+ LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent()
+ "; path to write="
+ + persistencePath);
}
/**
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketProtoUtils.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketProtoUtils.java
index 95808a7a855..1488d949def 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketProtoUtils.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketProtoUtils.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.NavigableSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.hadoop.hbase.io.ByteBuffAllocator;
import org.apache.hadoop.hbase.io.ByteBuffAllocator.Recycler;
@@ -64,6 +65,12 @@ final class BucketProtoUtils {
public static void serializeAsPB(BucketCache cache, FileOutputStream fos,
long chunkSize)
throws IOException {
+ serializeAsPB(cache, fos, chunkSize, entry -> {
+ });
+ }
+
+ static void serializeAsPB(BucketCache cache, FileOutputStream fos, long
chunkSize,
+ Consumer<Map.Entry<BlockCacheKey, BucketEntry>> entryCopiedAction) throws
IOException {
// Write the new version of magic number.
fos.write(PB_MAGIC_V2);
@@ -79,6 +86,7 @@ final class BucketProtoUtils {
for (Map.Entry<BlockCacheKey, BucketEntry> entry :
cache.backingMap.entrySet()) {
blockCount++;
addEntryToBuilder(entry, entryBuilder, builder);
+ entryCopiedAction.accept(entry);
if (blockCount % chunkSize == 0) {
builder.build().writeDelimitedTo(fos);
builder.clear();
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestBlockEvictionOnRegionMovement.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestBlockEvictionOnRegionMovement.java
index cb3f7e5772b..0063d8960f5 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestBlockEvictionOnRegionMovement.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestBlockEvictionOnRegionMovement.java
@@ -27,6 +27,7 @@ import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
import org.apache.hadoop.hbase.StartTestingClusterOption;
import org.apache.hadoop.hbase.TableName;
@@ -126,20 +127,24 @@ public class TestBlockEvictionOnRegionMovement {
: cluster.getRegionServer(0);
assertTrue(regionServingRS.getBlockCache().isPresent());
- long oldUsedCacheSize =
-
regionServingRS.getBlockCache().get().getBlockCaches()[1].getCurrentSize();
- assertNotEquals(0,
regionServingRS.getBlockCache().get().getBlockCaches()[1].getBlockCount());
+ BlockCache oldBucketCache =
regionServingRS.getBlockCache().get().getBlockCaches()[1];
+ long oldUsedCacheSize = oldBucketCache.getCurrentSize();
+ assertNotEquals(0, oldUsedCacheSize);
+ assertNotEquals(0, oldBucketCache.getBlockCount());
- cluster.stopRegionServer(regionServingRS.getServerName());
- Thread.sleep(500);
- cluster.startRegionServer();
- Thread.sleep(500);
+ ServerName serverName = regionServingRS.getServerName();
+ cluster.stopRegionServer(serverName);
+ cluster.waitForRegionServerToStop(serverName, 10000);
- regionServingRS.getBlockCache().get().waitForCacheInitialization(10000);
- long newUsedCacheSize =
-
regionServingRS.getBlockCache().get().getBlockCaches()[1].getCurrentSize();
- assertEquals(oldUsedCacheSize, newUsedCacheSize);
- assertNotEquals(0,
regionServingRS.getBlockCache().get().getBlockCaches()[1].getBlockCount());
+ assertEquals(0, oldBucketCache.getCurrentSize());
+
+ HRegionServer restartedRegionServer =
cluster.startRegionServer().getRegionServer();
+ assertTrue(restartedRegionServer.getBlockCache().isPresent());
+ BlockCache restoredBucketCache =
+ restartedRegionServer.getBlockCache().get().getBlockCaches()[1];
+ assertTrue(restoredBucketCache.waitForCacheInitialization(10000));
+ assertEquals(oldUsedCacheSize, restoredBucketCache.getCurrentSize());
+ assertNotEquals(0, restoredBucketCache.getBlockCount());
}
public TableName writeDataToTable(String testName) throws IOException,
InterruptedException {
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCache.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCache.java
index 6a5eac5fe3d..2908f3603e4 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCache.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCache.java
@@ -35,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
@@ -275,23 +276,36 @@ public class TestBucketCache {
final BlockCacheKey cacheKey = new BlockCacheKey("dummy", 1L);
cacheAndWaitUntilFlushedToBucket(cache, cacheKey,
new CacheTestUtils.ByteArrayCacheable(new byte[10]), true);
- long lockId = cache.backingMap.get(cacheKey).offset();
+ BucketEntry oldEntry = cache.backingMap.get(cacheKey);
+ long lockId = oldEntry.offset();
ReentrantReadWriteLock lock = cache.offsetLock.getLock(lockId);
- lock.writeLock().lock();
Thread evictThread = new Thread("evict-block") {
@Override
public void run() {
cache.evictBlock(cacheKey);
}
};
- evictThread.start();
- cache.offsetLock.waitForWaiters(lockId, 1);
- cache.blockEvicted(cacheKey, cache.backingMap.remove(cacheKey), true,
true);
- assertEquals(0, cache.getBlockCount());
- cacheAndWaitUntilFlushedToBucket(cache, cacheKey,
- new CacheTestUtils.ByteArrayCacheable(new byte[10]), true);
+ BucketEntry replacementEntry;
+ lock.writeLock().lock();
+ try {
+ evictThread.start();
+ cache.offsetLock.waitForWaiters(lockId, 1);
+ assertTrue(cache.backingMap.remove(cacheKey, oldEntry));
+ cache.blockEvicted(cacheKey, oldEntry, true, true);
+ assertEquals(0, cache.getBlockCount());
+ cache.cacheBlock(cacheKey, new CacheTestUtils.ByteArrayCacheable(new
byte[10]), false, true);
+ // The replacement is published before taking its offset lock. Full
flush must wait until the
+ // old entry's lock is released.
+ Waiter.waitFor(HBaseConfiguration.create(), 10000, () -> {
+ BucketEntry currentEntry = cache.backingMap.get(cacheKey);
+ return currentEntry != null && currentEntry != oldEntry;
+ });
+ replacementEntry = cache.backingMap.get(cacheKey);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ waitUntilFlushedToBucket(cache, cacheKey);
assertEquals(1, cache.getBlockCount());
- lock.writeLock().unlock();
evictThread.join();
/**
* <pre>
@@ -310,6 +324,8 @@ public class TestBucketCache {
* it had seen should not be evicted.
* </pre>
*/
+ assertSame(replacementEntry, cache.backingMap.get(cacheKey));
+ assertTrue(cache.blocksByHFile.contains(cacheKey));
assertEquals(1L, cache.getBlockCount());
assertTrue(cache.getCurrentSize() > 0L);
assertTrue(cache.iterator().hasNext(), "We should have a block!");
@@ -385,7 +401,11 @@ public class TestBucketCache {
}
usedSize = bucketCache.getAllocator().getUsedSize();
assertNotEquals(0, usedSize);
+ BucketEntry persistedEntry =
bucketCache.backingMap.values().iterator().next();
+ assertEquals(1, persistedEntry.refCnt());
bucketCache.shutdown();
+ assertEquals(0, persistedEntry.refCnt());
+ assertTrue(bucketCache.backingMap.isEmpty());
assertTrue(new File(persistencePath).exists());
bucketCache = new BucketCache(ioEngineName, capacitySize,
constructedBlockSize,
constructedBlockSizes, writeThreads, writerQLen, persistencePath);
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCacheRefCnt.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCacheRefCnt.java
index b466719136c..d6adb35d9cc 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCacheRefCnt.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCacheRefCnt.java
@@ -17,19 +17,34 @@
*/
package org.apache.hadoop.hbase.io.hfile.bucket;
+import static
org.apache.hadoop.hbase.io.hfile.CacheConfig.BUCKETCACHE_PERSIST_INTERVAL_KEY;
+import static
org.apache.hadoop.hbase.io.hfile.bucket.BucketCache.DEFAULT_ERROR_TOLERATION_DURATION;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.Waiter;
import org.apache.hadoop.hbase.io.ByteBuffAllocator;
@@ -45,9 +60,15 @@ import org.apache.hadoop.hbase.nio.ByteBuff;
import org.apache.hadoop.hbase.nio.RefCnt;
import org.apache.hadoop.hbase.testclassification.IOTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.hadoop.hbase.util.ManualEnvironmentEdge;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
+
+import org.apache.hadoop.hbase.shaded.protobuf.generated.BucketCacheProtos;
@Tag(IOTests.TAG)
@Tag(SmallTests.TAG)
@@ -306,6 +327,333 @@ public class TestBucketCacheRefCnt {
}
}
+ @Test
+ public void testShutdownReleasesOnlyBackingMapReference() throws Exception {
+ ByteBuffAllocator allocator =
ByteBuffAllocator.create(HBaseConfiguration.create(), true);
+ HFileBlock blockToCache = createBlock(200, 1020, allocator);
+ HFileBlock retainedBlock = null;
+ try {
+ cache = create(1, 1000);
+ BlockCacheKey key =
createKey("testShutdownReleasesOnlyBackingMapReference", 200);
+ cache.cacheBlock(key, blockToCache);
+ waitUntilFlushedToCache(cache, key);
+
+ retainedBlock = (HFileBlock) cache.getBlock(key, false, false, false);
+ assertNotNull(retainedBlock);
+ assertEquals(2, retainedBlock.refCnt());
+
+ HFileBlock callerReference = retainedBlock;
+ cache.shutdown();
+ cache.shutdown();
+
+ Waiter.waitFor(HBaseConfiguration.create(), 10000,
+ () -> cache.backingMap.isEmpty() && callerReference.refCnt() == 1);
+ assertEquals(1, retainedBlock.refCnt());
+ assertTrue(retainedBlock.release());
+ assertEquals(0, retainedBlock.refCnt());
+ } finally {
+ if (cache != null) {
+ cache.shutdown();
+ cache = null;
+ }
+ if (retainedBlock != null) {
+ while (retainedBlock.refCnt() > 0) {
+ retainedBlock.release();
+ }
+ }
+ while (blockToCache.refCnt() > 0) {
+ blockToCache.release();
+ }
+ allocator.clean();
+ }
+ }
+
+ @Test
+ public void testShutdownPersistsEmptyMapOverPreviousCheckpoint(@TempDir File
testDir)
+ throws Exception {
+ HFileBlock blockToCache = createBlock(200, 1020);
+ BucketCache bucketCache = null;
+ BucketCache recoveredCache = null;
+ String cachePath = new File(testDir, "bucket.cache").getAbsolutePath();
+ String persistencePath = new File(testDir,
"bucket.persistence").getAbsolutePath();
+ BlockCacheKey key =
createKey("testShutdownPersistsEmptyMapOverPreviousCheckpoint", 200);
+ Configuration conf = HBaseConfiguration.create();
+ conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
+ try {
+ bucketCache = new BucketCache("file:" + cachePath, CAPACITY_SIZE,
BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath,
DEFAULT_ERROR_TOLERATION_DURATION, conf);
+ assertTrue(bucketCache.waitForCacheInitialization(10000));
+ bucketCache.cacheBlock(key, blockToCache);
+ waitUntilFlushedToCache(bucketCache, key);
+
+ bucketCache.persistToFile();
+ assertTrue(new File(persistencePath).isFile());
+ assertTrue(bucketCache.backingMap.containsKey(key));
+ assertTrue(bucketCache.evictBlock(key));
+ assertTrue(bucketCache.backingMap.isEmpty());
+ bucketCache.shutdown();
+ try (
+ DataInputStream in = new DataInputStream(new FileInputStream(new
File(persistencePath)))) {
+ byte[] magic = new byte[BucketProtoUtils.PB_MAGIC_V2.length];
+ in.readFully(magic);
+ assertArrayEquals(BucketProtoUtils.PB_MAGIC_V2, magic);
+
assertNotNull(BucketCacheProtos.BucketCacheEntry.parseDelimitedFrom(in));
+ assertEquals(-1, in.read());
+ }
+ bucketCache = null;
+
+ recoveredCache = new BucketCache("file:" + cachePath, CAPACITY_SIZE,
BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath,
DEFAULT_ERROR_TOLERATION_DURATION, conf);
+ assertTrue(recoveredCache.waitForCacheInitialization(10000));
+ assertTrue(recoveredCache.backingMap.isEmpty());
+ assertEquals(0, recoveredCache.getAllocator().getUsedSize());
+ assertEquals(0, recoveredCache.getBlockCount());
+ Cacheable staleBlock = recoveredCache.getBlock(key, false, false, false);
+ if (staleBlock != null) {
+ staleBlock.release();
+ }
+ assertNull(staleBlock);
+ } finally {
+ if (bucketCache != null) {
+ bucketCache.shutdown();
+ }
+ if (recoveredCache != null) {
+ recoveredCache.shutdown();
+ }
+ while (blockToCache.refCnt() > 0) {
+ blockToCache.release();
+ }
+ }
+ }
+
+ @Test
+ public void testFinalPersistFailureReleasesEntriesAndKeepsPreviousCheckpoint(
+ @TempDir File testDir) throws Exception {
+ HFileBlock firstBlock = createBlock(200, 1020);
+ HFileBlock secondBlock = createBlock(400, 1020);
+ BucketCache bucketCache = null;
+ BucketCache recoveredCache = null;
+ String cachePath = new File(testDir, "bucket.cache").getAbsolutePath();
+ String persistencePath = new File(testDir,
"bucket.persistence").getAbsolutePath();
+ BlockCacheKey firstKey = createKey("first", 200);
+ BlockCacheKey secondKey = createKey("second", 400);
+ Configuration conf = HBaseConfiguration.create();
+ conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
+ try {
+ bucketCache = new BucketCache("file:" + cachePath, CAPACITY_SIZE,
BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath,
DEFAULT_ERROR_TOLERATION_DURATION, conf);
+ assertTrue(bucketCache.waitForCacheInitialization(10000));
+ bucketCache.cacheBlock(firstKey, firstBlock);
+ waitUntilFlushedToCache(bucketCache, firstKey);
+ bucketCache.persistToFile();
+ byte[] previousCheckpoint = Files.readAllBytes(new
File(persistencePath).toPath());
+
+ bucketCache.cacheBlock(secondKey, secondBlock);
+ waitUntilFlushedToCache(bucketCache, secondKey);
+ BucketEntry firstEntry = bucketCache.backingMap.get(firstKey);
+ BucketEntry secondEntry = bucketCache.backingMap.get(secondKey);
+ assertNotNull(firstEntry);
+ assertNotNull(secondEntry);
+ assertEquals(1, firstEntry.refCnt());
+ assertEquals(1, secondEntry.refCnt());
+
+ long failureTime = 123456789L;
+ File tempPersistencePath = new File(persistencePath + failureTime);
+ assertTrue(tempPersistencePath.mkdir());
+ ManualEnvironmentEdge edge = new ManualEnvironmentEdge();
+ edge.setValue(failureTime);
+ EnvironmentEdgeManager.injectEdge(edge);
+ try {
+ bucketCache.shutdown();
+ } finally {
+ EnvironmentEdgeManager.reset();
+ }
+
+ assertTrue(bucketCache.backingMap.isEmpty());
+ assertEquals(0, firstEntry.refCnt());
+ assertEquals(0, secondEntry.refCnt());
+ assertArrayEquals(previousCheckpoint, Files.readAllBytes(new
File(persistencePath).toPath()));
+ bucketCache = null;
+
+ recoveredCache = new BucketCache("file:" + cachePath, CAPACITY_SIZE,
BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath,
DEFAULT_ERROR_TOLERATION_DURATION, conf);
+ assertTrue(recoveredCache.waitForCacheInitialization(10000));
+ BucketCache cacheToValidate = recoveredCache;
+ Waiter.waitFor(HBaseConfiguration.create(), 10000,
+ () -> cacheToValidate.getBackingMapValidated().get());
+ assertEquals(1, recoveredCache.backingMap.size());
+ Cacheable recoveredBlock = recoveredCache.getBlock(firstKey, false,
false, false);
+ assertNotNull(recoveredBlock);
+ recoveredBlock.release();
+ Cacheable uncheckpointedBlock = recoveredCache.getBlock(secondKey,
false, false, false);
+ if (uncheckpointedBlock != null) {
+ uncheckpointedBlock.release();
+ }
+ assertNull(uncheckpointedBlock);
+ } finally {
+ EnvironmentEdgeManager.reset();
+ if (bucketCache != null) {
+ bucketCache.shutdown();
+ }
+ if (recoveredCache != null) {
+ recoveredCache.shutdown();
+ }
+ while (firstBlock.refCnt() > 0) {
+ firstBlock.release();
+ }
+ while (secondBlock.refCnt() > 0) {
+ secondBlock.release();
+ }
+ }
+ }
+
+ @Test
+ public void testHBaseIOExceptionThroughReferenceEvictsStoredEntry(@TempDir
File testDir)
+ throws Exception {
+ HFileBlock blockToCache = createBlock(200, 1020);
+ BucketEntry bucketEntry = null;
+ String cachePath = new File(testDir, "bucket.cache").getAbsolutePath();
+ String persistencePath = new File(testDir,
"bucket.persistence").getAbsolutePath();
+ BucketCache bucketCache = new BucketCache("file:" + cachePath,
CAPACITY_SIZE, BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath);
+ try {
+ assertTrue(bucketCache.waitForCacheInitialization(10000));
+ String hfileName = "0123456789abcdef";
+ String regionName = "region";
+ BlockCacheKey storedKey =
+ new BlockCacheKey(hfileName, "cf", regionName, 200, true,
BlockType.DATA, false);
+ BlockCacheKey referenceKey = createKey(hfileName + ".parent", 200);
+ bucketCache.cacheBlock(storedKey, blockToCache);
+ waitUntilFlushedToCache(bucketCache, storedKey);
+ bucketCache.fileCacheCompleted(new Path("/table/" + regionName + "/cf/"
+ hfileName), 1020);
+
+ bucketEntry = bucketCache.backingMap.get(storedKey);
+ assertNotNull(bucketEntry);
+ assertTrue(bucketCache.regionCachedSize.containsKey(regionName));
+ assertTrue(bucketCache.fullyCachedFiles.containsKey(hfileName));
+
+ ByteBuffer invalidCachedTime = ByteBuffer.allocate(Long.BYTES);
+ invalidCachedTime.putLong(bucketEntry.getCachedTime() + 1).flip();
+ bucketCache.ioEngine.write(invalidCachedTime, bucketEntry.offset());
+ bucketCache.ioEngine.sync();
+
+ assertNull(bucketCache.getBlock(referenceKey, false, false, false));
+ assertFalse(bucketCache.backingMap.containsKey(storedKey));
+ assertFalse(bucketCache.blocksByHFile.contains(storedKey));
+ assertFalse(bucketCache.regionCachedSize.containsKey(regionName));
+ assertFalse(bucketCache.fullyCachedFiles.containsKey(hfileName));
+ assertEquals(0, bucketEntry.refCnt());
+ assertEquals(0, bucketCache.getAllocator().getUsedSize());
+ } finally {
+ bucketCache.shutdown();
+ if (bucketEntry != null && bucketEntry.refCnt() > 0) {
+ bucketEntry.markAsEvicted();
+ }
+ while (blockToCache.refCnt() > 0) {
+ blockToCache.release();
+ }
+ }
+ }
+
+ @Test
+ public void testPlainIOExceptionKeepsEntryAndCacheMetadata(@TempDir File
testDir)
+ throws Exception {
+ HFileBlock blockToCache = createBlock(200, 1020);
+ String cachePath = new File(testDir, "bucket.cache").getAbsolutePath();
+ String persistencePath = new File(testDir,
"bucket.persistence").getAbsolutePath();
+ BucketCache bucketCache = new BucketCache("file:" + cachePath,
CAPACITY_SIZE, BLOCK_SIZE,
+ BLOCK_SIZE_ARRAY, 1, 1000, persistencePath);
+ FileChannel originalChannel = null;
+ try {
+ assertTrue(bucketCache.waitForCacheInitialization(10000));
+ String hfileName = "testPlainIOExceptionKeepsEntryAndCacheMetadata";
+ String regionName = "region";
+ BlockCacheKey key =
+ new BlockCacheKey(hfileName, "cf", regionName, 200, true,
BlockType.DATA, false);
+ bucketCache.cacheBlock(key, blockToCache);
+ waitUntilFlushedToCache(bucketCache, key);
+ bucketCache.fileCacheCompleted(new Path("/table/" + regionName + "/cf/"
+ hfileName), 1020);
+
+ BucketEntry bucketEntry = bucketCache.backingMap.get(key);
+ assertNotNull(bucketEntry);
+ FileIOEngine fileIOEngine = (FileIOEngine) bucketCache.ioEngine;
+ originalChannel = fileIOEngine.getFileChannels()[0];
+ FileChannel failingChannel = Mockito.mock(FileChannel.class);
+ Mockito.when(failingChannel.read(Mockito.any(ByteBuffer.class),
Mockito.anyLong()))
+ .thenThrow(new IOException("Injected read failure"));
+ fileIOEngine.getFileChannels()[0] = failingChannel;
+
+ assertNull(bucketCache.getBlock(key, false, false, false));
+ assertTrue(bucketCache.isCacheEnabled());
+ assertEquals(bucketEntry, bucketCache.backingMap.get(key));
+ assertTrue(bucketCache.blocksByHFile.contains(key));
+ assertTrue(bucketCache.regionCachedSize.containsKey(regionName));
+ assertTrue(bucketCache.fullyCachedFiles.containsKey(hfileName));
+ assertEquals(1, bucketEntry.refCnt());
+ } finally {
+ if (originalChannel != null) {
+ ((FileIOEngine) bucketCache.ioEngine).getFileChannels()[0] =
originalChannel;
+ }
+ bucketCache.shutdown();
+ while (blockToCache.refCnt() > 0) {
+ blockToCache.release();
+ }
+ }
+ }
+
+ @Test
+ public void
testInitialPublicationDoesNotRestoreIndexAfterConcurrentEviction() throws
Exception {
+ BucketCache bucketCache = create(1, 1000);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ CountDownLatch entryPublished = new CountDownLatch(1);
+ CountDownLatch continuePublication = new CountDownLatch(1);
+ BucketEntry publishedEntry = null;
+ try {
+ BlockCacheKey key =
+
createKey("testInitialPublicationDoesNotRestoreIndexAfterConcurrentEviction",
200);
+ publishedEntry = new BucketEntry(8192, 1020, 1020, 0, false, entry ->
ByteBuffAllocator.NONE,
+ ByteBuffAllocator.HEAP);
+ BucketEntry entryToPublish = publishedEntry;
+ bucketCache.backingMap = Mockito.spy(bucketCache.backingMap);
+ Mockito.doAnswer(invocation -> {
+ Object previousEntry = invocation.callRealMethod();
+ entryPublished.countDown();
+ if (!continuePublication.await(10, TimeUnit.SECONDS)) {
+ throw new AssertionError("Timed out waiting to resume publication");
+ }
+ return previousEntry;
+ }).when(bucketCache.backingMap).put(key, entryToPublish);
+ Future<?> publication =
+ executor.submit(() -> bucketCache.putIntoBackingMap(key,
entryToPublish));
+
+ assertTrue(entryPublished.await(10, TimeUnit.SECONDS));
+ assertTrue(entryToPublish.withWriteLock(bucketCache.offsetLock, () -> {
+ if (bucketCache.backingMap.remove(key, entryToPublish)) {
+ bucketCache.blockEvicted(key, entryToPublish, false, false);
+ return true;
+ }
+ return false;
+ }));
+ continuePublication.countDown();
+ publication.get(10, TimeUnit.SECONDS);
+
+ assertFalse(bucketCache.backingMap.containsKey(key));
+ assertFalse(bucketCache.blocksByHFile.contains(key));
+ assertEquals(0, publishedEntry.refCnt());
+ } finally {
+ continuePublication.countDown();
+ executor.shutdownNow();
+ try {
+ assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+ } finally {
+ bucketCache.shutdown();
+ if (publishedEntry != null && publishedEntry.refCnt() > 0) {
+ publishedEntry.markAsEvicted();
+ }
+ }
+ }
+ }
+
/**
* <pre>
* This test is for HBASE-26281,
@@ -372,6 +720,7 @@ public class TestBucketCacheRefCnt {
cacheBlockThread.join();
assertTrue(exceptionRef.get() == null);
+ assertTrue(myBucketCache.blocksByHFile.contains(blockCacheKey));
assertEquals(1, gotHFileBlock.refCnt());
assertTrue(gotHFileBlock.equals(hfileBlock));
assertTrue(myBucketCache.overwiteByteBuff == null);