9292import org .apache .hadoop .hbase .util .IdReadWriteLockWithObjectPool ;
9393import org .apache .hadoop .hbase .util .IdReadWriteLockWithObjectPool .ReferenceType ;
9494import org .apache .hadoop .hbase .util .Pair ;
95+ import org .apache .hadoop .hbase .util .Threads ;
9596import org .apache .hadoop .util .StringUtils ;
9697import org .apache .yetus .audience .InterfaceAudience ;
9798import org .slf4j .Logger ;
@@ -206,6 +207,12 @@ 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+
213+ /** The thread restoring the persistent cache index during initialization. */
214+ private volatile Thread persistenceRetrieverThread ;
215+
209216 /**
210217 * A list of writer queues. We have a queue per {@link WriterThread} we have running. In other
211218 * words, the work adding blocks to the BucketCache is divided up amongst the running
@@ -416,12 +423,17 @@ private void startPersistenceRetriever(int[] bucketSizes, long capacity) {
416423 LOG .error ("Exception during Bucket Allocation" , allocatorException );
417424 }
418425 } finally {
419- this .cacheState = CacheState .ENABLED ;
420- startWriterThreads ();
426+ synchronized (BucketCache .this ) {
427+ if (cacheState == CacheState .INITIALIZING ) {
428+ cacheState = CacheState .ENABLED ;
429+ startWriterThreads ();
430+ }
431+ }
421432 }
422433 };
423- Thread t = new Thread (persistentCacheRetriever );
424- t .start ();
434+ persistenceRetrieverThread = new Thread (persistentCacheRetriever ,
435+ "BucketCachePersistenceRetriever-" + System .identityHashCode (this ));
436+ persistenceRetrieverThread .start ();
425437 }
426438
427439 private void sanityCheckConfigs () {
@@ -643,21 +655,24 @@ protected void cacheBlockWithWaitInternal(BlockCacheKey cacheKey, Cacheable cach
643655 * the passed key doesn't relate to a reference.
644656 */
645657 public BucketEntry getBlockForReference (BlockCacheKey key ) {
646- BucketEntry foundEntry = null ;
647- String referredFileName = null ;
648- if (StoreFileInfo .isReference (key .getHfileName ())) {
649- referredFileName = StoreFileInfo .getReferredToRegionAndFile (key .getHfileName ()).getSecond ();
650- }
651- if (referredFileName != null ) {
652- // Since we just need this key for a lookup, it's enough to use only name and offset
653- BlockCacheKey convertedCacheKey = new BlockCacheKey (referredFileName , key .getOffset ());
654- foundEntry = backingMap .get (convertedCacheKey );
658+ BlockCacheKey referredKey = getBlockKeyForReference (key );
659+ BucketEntry foundEntry = referredKey != null ? backingMap .get (referredKey ) : null ;
660+ if (referredKey != null ) {
655661 LOG .debug ("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}" , key .getHfileName (),
656- convertedCacheKey , foundEntry );
662+ referredKey , foundEntry );
657663 }
658664 return foundEntry ;
659665 }
660666
667+ private BlockCacheKey getBlockKeyForReference (BlockCacheKey key ) {
668+ if (!StoreFileInfo .isReference (key .getHfileName ())) {
669+ return null ;
670+ }
671+ String referredFileName =
672+ StoreFileInfo .getReferredToRegionAndFile (key .getHfileName ()).getSecond ();
673+ return referredFileName != null ? new BlockCacheKey (referredFileName , key .getOffset ()) : null ;
674+ }
675+
661676 /**
662677 * Get the buffer of the block with the specified key.
663678 * @param key block's cache key
@@ -681,23 +696,28 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
681696 re .access (accessCount .incrementAndGet ());
682697 return re .getData ();
683698 }
684- BucketEntry bucketEntry = backingMap .get (key );
699+ BlockCacheKey backingMapLookupKey = key ;
700+ BucketEntry bucketEntry = backingMap .get (backingMapLookupKey );
685701 LOG .debug ("bucket entry for key {}: {}" , key ,
686702 bucketEntry == null ? null : bucketEntry .offset ());
687703 if (bucketEntry == null ) {
688- bucketEntry = getBlockForReference (key );
704+ backingMapLookupKey = getBlockKeyForReference (key );
705+ if (backingMapLookupKey != null ) {
706+ bucketEntry = backingMap .get (backingMapLookupKey );
707+ LOG .debug ("Got a link/ref: {}. Related cacheKey: {}. Found entry: {}" , key .getHfileName (),
708+ backingMapLookupKey , bucketEntry );
709+ }
689710 }
690711 if (bucketEntry != null ) {
691712 long start = System .nanoTime ();
692713 ReentrantReadWriteLock lock = offsetLock .getLock (bucketEntry .offset ());
714+ boolean inconsistentEntry = false ;
693715 try {
694716 lock .readLock ().lock ();
695717 // We can not read here even if backingMap does contain the given key because its offset
696718 // maybe changed. If we lock BlockCacheKey instead of offset, then we can only check
697719 // existence here.
698- if (
699- bucketEntry .equals (backingMap .get (key )) || bucketEntry .equals (getBlockForReference (key ))
700- ) {
720+ if (bucketEntry .equals (backingMap .get (backingMapLookupKey ))) {
701721 // Read the block from IOEngine based on the bucketEntry's offset and length, NOTICE: the
702722 // block will use the refCnt of bucketEntry, which means if two HFileBlock mapping to
703723 // the same BucketEntry, then all of the three will share the same refCnt.
@@ -719,25 +739,39 @@ public Cacheable getBlock(BlockCacheKey key, boolean caching, boolean repeat,
719739 return cachedBlock ;
720740 }
721741 } catch (HBaseIOException hioex ) {
722- // When using file io engine persistent cache,
723- // the cache map state might differ from the actual cache. If we reach this block,
724- // we should remove the cache key entry from the backing map
725- backingMap .remove (key );
726- fileNotFullyCached (key , bucketEntry );
742+ // FileIOEngine throws this when its cached time differs from the persisted index. A plain
743+ // IOException still follows the configured tolerance policy below.
744+ inconsistentEntry = true ;
727745 LOG .debug ("Failed to fetch block for cache key: {}." , key , hioex );
728746 } catch (IOException ioex ) {
729747 LOG .error ("Failed reading block " + key + " from bucket cache" , ioex );
730748 checkIOErrorIsTolerated ();
731749 } finally {
732750 lock .readLock ().unlock ();
733751 }
752+ if (inconsistentEntry ) {
753+ evictInconsistentEntry (backingMapLookupKey , bucketEntry );
754+ }
734755 }
735756 if (!repeat && updateCacheMetrics ) {
736757 cacheStats .miss (caching , key .isPrimary (), key .getBlockType ());
737758 }
738759 return null ;
739760 }
740761
762+ private void evictInconsistentEntry (BlockCacheKey lookupKey , BucketEntry bucketEntry ) {
763+ BlockCacheKey storedKey = blocksByHFile .ceiling (lookupKey );
764+ if (storedKey == null || !storedKey .equals (lookupKey )) {
765+ return ;
766+ }
767+ bucketEntry .withWriteLock (offsetLock , () -> {
768+ if (backingMap .remove (storedKey , bucketEntry )) {
769+ blockEvicted (storedKey , bucketEntry , true , false );
770+ }
771+ return null ;
772+ });
773+ }
774+
741775 /**
742776 * This method is invoked after the bucketEntry is removed from {@link BucketCache#backingMap}
743777 */
@@ -1350,14 +1384,19 @@ public void run() {
13501384 */
13511385 protected void putIntoBackingMap (BlockCacheKey key , BucketEntry bucketEntry ) {
13521386 BucketEntry previousEntry = backingMap .put (key , bucketEntry );
1353- blocksByHFile .add (key );
13541387 updateRegionCachedSize (key , bucketEntry .getLength ());
13551388 if (previousEntry != null && previousEntry != bucketEntry ) {
13561389 previousEntry .withWriteLock (offsetLock , () -> {
13571390 blockEvicted (key , previousEntry , false , false );
13581391 return null ;
13591392 });
13601393 }
1394+ bucketEntry .withWriteLock (offsetLock , () -> {
1395+ if (backingMap .get (key ) == bucketEntry ) {
1396+ blocksByHFile .add (key );
1397+ }
1398+ return null ;
1399+ });
13611400 }
13621401
13631402 /**
@@ -1551,6 +1590,12 @@ static List<RAMQueueEntry> getRAMQueueEntries(BlockingQueue<RAMQueueEntry> q,
15511590 @ edu .umd .cs .findbugs .annotations .SuppressWarnings (value = "OBL_UNSATISFIED_OBLIGATION" ,
15521591 justification = "false positive, try-with-resources ensures close is called." )
15531592 void persistToFile () throws IOException {
1593+ persistToFile (entry -> {
1594+ });
1595+ }
1596+
1597+ private void persistToFile (Consumer <Map .Entry <BlockCacheKey , BucketEntry >> entryCopiedAction )
1598+ throws IOException {
15541599 LOG .debug ("Thread {} started persisting bucket cache to file" ,
15551600 Thread .currentThread ().getName ());
15561601 if (!isCachePersistent ()) {
@@ -1560,7 +1605,7 @@ void persistToFile() throws IOException {
15601605 try (FileOutputStream fos = new FileOutputStream (tempPersistencePath , false )) {
15611606 LOG .debug ("Persist in new chunked persistence format." );
15621607
1563- persistChunkedBackingMap (fos );
1608+ persistChunkedBackingMap (fos , entryCopiedAction );
15641609
15651610 LOG .debug (
15661611 "PersistToFile: after persisting backing map size: {}, fullycachedFiles size: {},"
@@ -1740,13 +1785,14 @@ private void parsePB(BucketCacheProtos.BucketCacheEntry proto) throws IOExceptio
17401785 verifyCapacityAndClasses (proto .getCacheCapacity (), proto .getIoClass (), proto .getMapClass ());
17411786 }
17421787
1743- private void persistChunkedBackingMap (FileOutputStream fos ) throws IOException {
1788+ private void persistChunkedBackingMap (FileOutputStream fos ,
1789+ Consumer <Map .Entry <BlockCacheKey , BucketEntry >> entryCopiedAction ) throws IOException {
17441790 LOG .debug (
17451791 "persistToFile: before persisting backing map size: {}, "
17461792 + "fullycachedFiles size: {}, chunkSize: {}" ,
17471793 backingMap .size (), fullyCachedFiles .size (), persistenceChunkSize );
17481794
1749- BucketProtoUtils .serializeAsPB (this , fos , persistenceChunkSize );
1795+ BucketProtoUtils .serializeAsPB (this , fos , persistenceChunkSize , entryCopiedAction );
17501796
17511797 LOG .debug (
17521798 "persistToFile: after persisting backing map size: {}, " + "fullycachedFiles size: {}" ,
@@ -1807,59 +1853,106 @@ private void checkIOErrorIsTolerated() {
18071853
18081854 /**
18091855 * Used to shut down the cache -or- turn it off in the case of something broken.
1856+ * @return whether explicit shutdown should wait for cleanup
18101857 */
1811- private void disableCache () {
1812- if (! isCacheEnabled () ) {
1813- return ;
1858+ private synchronized boolean disableCache () {
1859+ if (cacheState == CacheState . DISABLED ) {
1860+ return false ;
18141861 }
1862+ boolean waitForCleanup = cacheState == CacheState .ENABLED && isCachePersistent ();
18151863 LOG .info ("Disabling cache" );
18161864 cacheState = CacheState .DISABLED ;
1817- ioEngine .shutdown ();
18181865 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 ();
1866+ for (WriterThread writerThread : writerThreads ) {
1867+ writerThread .interrupt ();
18281868 }
1869+ // Closing the IO engine helps unblock an in-flight writer before the cleanup thread joins it.
1870+ // FileIOEngine can reopen a channel, so cleanup closes the engine again after writers stop.
1871+ ioEngine .shutdown ();
18291872 if (cacheStats .getMetricsRollerScheduler () != null ) {
18301873 cacheStats .getMetricsRollerScheduler ().shutdownNow ();
18311874 }
1875+ cacheCleanupThread = Threads .setDaemonThreadRunning (new Thread (this ::cleanupCache ),
1876+ "BucketCacheCleanup-" + System .identityHashCode (this ), Threads .LOGGING_EXCEPTION_HANDLER );
1877+ return waitForCleanup ;
18321878 }
18331879
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 ) {
1880+ private void cleanupCache () {
1881+ try {
1882+ Threads .shutdown (persistenceRetrieverThread );
1883+ for (WriterThread writerThread : writerThreads ) {
1884+ Threads .shutdown (writerThread );
1885+ }
1886+ for (BlockingQueue <RAMQueueEntry > writerQueue : writerQueues ) {
1887+ writerQueue .clear ();
1888+ }
1889+ ramCache .clear ();
1890+ if (cachePersister != null ) {
1891+ LOG .info ("Shutting down cache persister thread." );
1892+ cachePersister .shutdown ();
1893+ Threads .shutdown (cachePersister );
1894+ }
1895+ if (isCachePersistent ()) {
18461896 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- }
1855- persistToFile ();
1897+ // The serializer already visits every entry. Release owner references in the same pass.
1898+ persistToFile (this ::cleanupBackingMapEntry );
18561899 } catch (IOException ex ) {
18571900 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 );
18601901 }
18611902 }
1903+ } finally {
1904+ try {
1905+ cleanupCacheIndex ();
1906+ } finally {
1907+ ioEngine .shutdown ();
1908+ }
1909+ }
1910+ }
1911+
1912+ private void cleanupCacheIndex () {
1913+ // A successful persistent cleanup emptied the map during serialization. Avoid creating a
1914+ // second iterator over a large ConcurrentHashMap in that case.
1915+ if (!backingMap .isEmpty ()) {
1916+ for (Map .Entry <BlockCacheKey , BucketEntry > entry : backingMap .entrySet ()) {
1917+ cleanupBackingMapEntry (entry );
1918+ }
1919+ }
1920+ blocksByHFile .clear ();
1921+ fullyCachedFiles .clear ();
1922+ regionCachedSize .clear ();
1923+ }
1924+
1925+ private void cleanupBackingMapEntry (Map .Entry <BlockCacheKey , BucketEntry > entry ) {
1926+ BlockCacheKey cacheKey = entry .getKey ();
1927+ BucketEntry bucketEntry = entry .getValue ();
1928+ bucketEntry .withWriteLock (offsetLock , () -> {
1929+ if (backingMap .remove (cacheKey , bucketEntry )) {
1930+ bucketEntry .markAsEvicted ();
1931+ }
1932+ return null ;
1933+ });
1934+ }
1935+
1936+ private void waitForCacheCleanup () throws InterruptedException {
1937+ Thread cleanupThread = cacheCleanupThread ;
1938+ if (cleanupThread == null || cleanupThread == Thread .currentThread ()) {
1939+ return ;
1940+ }
1941+ cleanupThread .join ();
1942+ }
1943+
1944+ @ Override
1945+ public void shutdown () {
1946+ if (disableCache ()) {
1947+ try {
1948+ waitForCacheCleanup ();
1949+ } catch (InterruptedException e ) {
1950+ Thread .currentThread ().interrupt ();
1951+ LOG .warn ("Interrupted while waiting for bucket cache cleanup" , e );
1952+ }
18621953 }
1954+ LOG .info ("Shutdown bucket cache: IO persistent=" + ioEngine .isPersistent () + "; path to write="
1955+ + persistencePath );
18631956 }
18641957
18651958 /**
0 commit comments