Skip to content

Commit 24afefa

Browse files
committed
HBASE-30285 BucketCache shutdown/disable does not release backingMap-owned BucketEntry references
1 parent f9a41c7 commit 24afefa

3 files changed

Lines changed: 454 additions & 36 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/bucket/BucketCache.java

Lines changed: 107 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool;
9393
import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType;
9494
import org.apache.hadoop.hbase.util.Pair;
95+
import org.apache.hadoop.hbase.util.Threads;
9596
import org.apache.hadoop.util.StringUtils;
9697
import org.apache.yetus.audience.InterfaceAudience;
9798
import org.slf4j.Logger;
@@ -206,6 +207,9 @@ protected enum CacheState {
206207
*/
207208
private volatile CacheState cacheState;
208209

210+
/** The single cleanup thread shared by disable and explicit shutdown calls. */
211+
private volatile Thread cacheCleanupThread;
212+
209213
/**
210214
* A list of writer queues. We have a queue per {@link WriterThread} we have running. In other
211215
* words, the work adding blocks to the BucketCache is divided up amongst the running
@@ -690,6 +694,7 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
690694
if (bucketEntry != null) {
691695
long start = System.nanoTime();
692696
ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset());
697+
boolean failedBucketEntryRead = false;
693698
try {
694699
lock.readLock().lock();
695700
// We can not read here even if backingMap does contain the given key because its offset
@@ -722,22 +727,48 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
722727
// When using file io engine persistent cache,
723728
// the cache map state might differ from the actual cache. If we reach this block,
724729
// we should remove the cache key entry from the backing map
725-
backingMap.remove(key);
726-
fileNotFullyCached(key, bucketEntry);
730+
failedBucketEntryRead = true;
727731
LOG.debug("Failed to fetch block for cache key: {}.", key, hioex);
728732
} catch (IOException ioex) {
729733
LOG.error("Failed reading block " + key + " from bucket cache", ioex);
730734
checkIOErrorIsTolerated();
731735
} finally {
732736
lock.readLock().unlock();
733737
}
738+
if (failedBucketEntryRead) {
739+
removeFailedBucketEntry(bucketEntry);
740+
}
734741
}
735742
if (!repeat && updateCacheMetrics) {
736743
cacheStats.miss(caching, key.isPrimary(), key.getBlockType());
737744
}
738745
return null;
739746
}
740747

748+
private void removeFailedBucketEntry(BucketEntry bucketEntry) {
749+
BlockCacheKey cacheKey = findBackingMapKey(bucketEntry);
750+
if (cacheKey == null) {
751+
return;
752+
}
753+
bucketEntry.withWriteLock(offsetLock, () -> {
754+
if (backingMap.remove(cacheKey, bucketEntry)) {
755+
blockEvicted(cacheKey, bucketEntry, true, false);
756+
}
757+
return null;
758+
});
759+
}
760+
761+
private BlockCacheKey findBackingMapKey(BucketEntry bucketEntry) {
762+
// Reference file lookups use a different key from the one stored in backingMap. This only runs
763+
// after a failed read, so find the stored key to preserve its region metadata during eviction.
764+
for (Map.Entry<BlockCacheKey, BucketEntry> entry : backingMap.entrySet()) {
765+
if (entry.getValue() == bucketEntry) {
766+
return entry.getKey();
767+
}
768+
}
769+
return null;
770+
}
771+
741772
/**
742773
* This method is invoked after the bucketEntry is removed from {@link BucketCache#backingMap}
743774
*/
@@ -1798,70 +1829,110 @@ private void checkIOErrorIsTolerated() {
17981829
if (isCacheEnabled() && (now - ioErrorStartTimeTmp) > this.ioErrorsTolerationDuration) {
17991830
LOG.error("IO errors duration time has exceeded " + ioErrorsTolerationDuration
18001831
+ "ms, disabling cache, please check your IOEngine");
1801-
disableCache();
1832+
disableCache(false);
18021833
}
18031834
} else {
18041835
this.ioErrorStartTime = now;
18051836
}
18061837
}
18071838

18081839
/**
1809-
* Used to shut down the cache -or- turn it off in the case of something broken.
1840+
* Used to shut down the cache -or- turn it off in the case of something broken. Cleanup runs
1841+
* separately because IO errors can invoke this method from a writer thread or while holding an
1842+
* offset read lock.
18101843
*/
1811-
private void disableCache() {
1844+
private synchronized void disableCache(boolean persistOnCleanup) {
18121845
if (!isCacheEnabled()) {
18131846
return;
18141847
}
18151848
LOG.info("Disabling cache");
18161849
cacheState = CacheState.DISABLED;
18171850
ioEngine.shutdown();
18181851
this.scheduleThreadPool.shutdown();
1819-
for (int i = 0; i < writerThreads.length; ++i)
1820-
writerThreads[i].interrupt();
1821-
this.ramCache.clear();
1822-
if (!ioEngine.isPersistent() || persistencePath == null) {
1823-
// If persistent ioengine and a path, we will serialize out the backingMap.
1824-
this.backingMap.clear();
1825-
this.blocksByHFile.clear();
1826-
this.fullyCachedFiles.clear();
1827-
this.regionCachedSize.clear();
1852+
for (WriterThread writerThread : writerThreads) {
1853+
writerThread.interrupt();
18281854
}
1855+
ramCache.clear();
18291856
if (cacheStats.getMetricsRollerScheduler() != null) {
18301857
cacheStats.getMetricsRollerScheduler().shutdownNow();
18311858
}
1859+
cacheCleanupThread =
1860+
Threads.setDaemonThreadRunning(new Thread(() -> cleanupCache(persistOnCleanup)),
1861+
"BucketCacheCleanup-" + System.identityHashCode(this), Threads.LOGGING_EXCEPTION_HANDLER);
18321862
}
18331863

1834-
private void join() throws InterruptedException {
1835-
for (int i = 0; i < writerThreads.length; ++i)
1836-
writerThreads[i].join();
1837-
}
1838-
1839-
@Override
1840-
public void shutdown() {
1841-
if (isCacheEnabled()) {
1842-
disableCache();
1843-
LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent()
1844-
+ "; path to write=" + persistencePath);
1845-
if (ioEngine.isPersistent() && persistencePath != null) {
1864+
private void cleanupCache(boolean persistOnCleanup) {
1865+
try {
1866+
joinWriterThreads();
1867+
stopCachePersister();
1868+
if (persistOnCleanup && isCachePersistent()) {
18461869
try {
1847-
join();
1848-
if (cachePersister != null) {
1849-
LOG.info("Shutting down cache persister thread.");
1850-
cachePersister.shutdown();
1851-
while (cachePersister.isAlive()) {
1852-
Thread.sleep(10);
1853-
}
1854-
}
18551870
persistToFile();
18561871
} catch (IOException ex) {
18571872
LOG.error("Unable to persist data on exit: " + ex.toString(), ex);
1858-
} catch (InterruptedException e) {
1859-
LOG.warn("Failed to persist data on exit", e);
18601873
}
18611874
}
1875+
} finally {
1876+
releaseBackingMapReferences();
1877+
}
1878+
}
1879+
1880+
private void joinWriterThreads() {
1881+
for (WriterThread writerThread : writerThreads) {
1882+
Threads.shutdown(writerThread);
1883+
}
1884+
}
1885+
1886+
private void stopCachePersister() {
1887+
if (cachePersister != null) {
1888+
LOG.info("Shutting down cache persister thread.");
1889+
cachePersister.shutdown();
1890+
Threads.shutdown(cachePersister);
1891+
}
1892+
}
1893+
1894+
private void releaseBackingMapReferences() {
1895+
for (Map.Entry<BlockCacheKey, BucketEntry> entry : backingMap.entrySet()) {
1896+
BlockCacheKey cacheKey = entry.getKey();
1897+
BucketEntry bucketEntry = entry.getValue();
1898+
bucketEntry.withWriteLock(offsetLock, () -> {
1899+
if (backingMap.remove(cacheKey, bucketEntry)) {
1900+
bucketEntry.markAsEvicted();
1901+
}
1902+
return null;
1903+
});
1904+
}
1905+
blocksByHFile.clear();
1906+
fullyCachedFiles.clear();
1907+
regionCachedSize.clear();
1908+
}
1909+
1910+
private void waitForCacheCleanup() {
1911+
Thread cleanupThread = cacheCleanupThread;
1912+
if (cleanupThread == null || cleanupThread == Thread.currentThread()) {
1913+
return;
1914+
}
1915+
boolean interrupted = false;
1916+
while (cleanupThread.isAlive()) {
1917+
try {
1918+
cleanupThread.join();
1919+
} catch (InterruptedException e) {
1920+
interrupted = true;
1921+
}
1922+
}
1923+
if (interrupted) {
1924+
Thread.currentThread().interrupt();
18621925
}
18631926
}
18641927

1928+
@Override
1929+
public void shutdown() {
1930+
disableCache(true);
1931+
waitForCacheCleanup();
1932+
LOG.info("Shutdown bucket cache: IO persistent=" + ioEngine.isPersistent() + "; path to write="
1933+
+ persistencePath);
1934+
}
1935+
18651936
/**
18661937
* Needed mostly for UTs that might run in the same VM and create different BucketCache instances
18671938
* on different UT methods.

hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/bucket/TestBucketCache.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,10 +841,15 @@ public void testFreeBucketEntryRestoredFromFile() throws Exception {
841841
cacheAndWaitUntilFlushedToBucket(bucketCache, hfileBlockPair.getBlockName(),
842842
hfileBlockPair.getBlock(), false);
843843
}
844+
BucketEntry bucketEntryBeforeShutdown =
845+
bucketCache.backingMap.get(hfileBlockPairs[0].getBlockName());
846+
assertNotNull(bucketEntryBeforeShutdown);
847+
assertEquals(1, bucketEntryBeforeShutdown.refCnt());
844848
usedByteSize = bucketCache.getAllocator().getUsedSize();
845849
assertNotEquals(0, usedByteSize);
846850
// persist cache to file
847851
bucketCache.shutdown();
852+
assertEquals(0, bucketEntryBeforeShutdown.refCnt());
848853
assertTrue(new File(persistencePath).exists());
849854

850855
// restore cache from file

0 commit comments

Comments
 (0)