|
92 | 92 | import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool; |
93 | 93 | import org.apache.hadoop.hbase.util.IdReadWriteLockWithObjectPool.ReferenceType; |
94 | 94 | import org.apache.hadoop.hbase.util.Pair; |
| 95 | +import org.apache.hadoop.hbase.util.Threads; |
95 | 96 | import org.apache.hadoop.util.StringUtils; |
96 | 97 | import org.apache.yetus.audience.InterfaceAudience; |
97 | 98 | import org.slf4j.Logger; |
@@ -206,6 +207,9 @@ protected enum CacheState { |
206 | 207 | */ |
207 | 208 | private volatile CacheState cacheState; |
208 | 209 |
|
| 210 | + /** The single cleanup thread shared by disable and explicit shutdown calls. */ |
| 211 | + private volatile Thread cacheCleanupThread; |
| 212 | + |
209 | 213 | /** |
210 | 214 | * A list of writer queues. We have a queue per {@link WriterThread} we have running. In other |
211 | 215 | * 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, |
690 | 694 | if (bucketEntry != null) { |
691 | 695 | long start = System.nanoTime(); |
692 | 696 | ReentrantReadWriteLock lock = offsetLock.getLock(bucketEntry.offset()); |
| 697 | + boolean failedBucketEntryRead = false; |
693 | 698 | try { |
694 | 699 | lock.readLock().lock(); |
695 | 700 | // 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, |
722 | 727 | // When using file io engine persistent cache, |
723 | 728 | // the cache map state might differ from the actual cache. If we reach this block, |
724 | 729 | // we should remove the cache key entry from the backing map |
725 | | - backingMap.remove(key); |
726 | | - fileNotFullyCached(key, bucketEntry); |
| 730 | + failedBucketEntryRead = true; |
727 | 731 | LOG.debug("Failed to fetch block for cache key: {}.", key, hioex); |
728 | 732 | } catch (IOException ioex) { |
729 | 733 | LOG.error("Failed reading block " + key + " from bucket cache", ioex); |
730 | 734 | checkIOErrorIsTolerated(); |
731 | 735 | } finally { |
732 | 736 | lock.readLock().unlock(); |
733 | 737 | } |
| 738 | + if (failedBucketEntryRead) { |
| 739 | + removeFailedBucketEntry(bucketEntry); |
| 740 | + } |
734 | 741 | } |
735 | 742 | if (!repeat && updateCacheMetrics) { |
736 | 743 | cacheStats.miss(caching, key.isPrimary(), key.getBlockType()); |
737 | 744 | } |
738 | 745 | return null; |
739 | 746 | } |
740 | 747 |
|
| 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 | + |
741 | 772 | /** |
742 | 773 | * This method is invoked after the bucketEntry is removed from {@link BucketCache#backingMap} |
743 | 774 | */ |
@@ -1798,70 +1829,110 @@ private void checkIOErrorIsTolerated() { |
1798 | 1829 | if (isCacheEnabled() && (now - ioErrorStartTimeTmp) > this.ioErrorsTolerationDuration) { |
1799 | 1830 | LOG.error("IO errors duration time has exceeded " + ioErrorsTolerationDuration |
1800 | 1831 | + "ms, disabling cache, please check your IOEngine"); |
1801 | | - disableCache(); |
| 1832 | + disableCache(false); |
1802 | 1833 | } |
1803 | 1834 | } else { |
1804 | 1835 | this.ioErrorStartTime = now; |
1805 | 1836 | } |
1806 | 1837 | } |
1807 | 1838 |
|
1808 | 1839 | /** |
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. |
1810 | 1843 | */ |
1811 | | - private void disableCache() { |
| 1844 | + private synchronized void disableCache(boolean persistOnCleanup) { |
1812 | 1845 | if (!isCacheEnabled()) { |
1813 | 1846 | return; |
1814 | 1847 | } |
1815 | 1848 | LOG.info("Disabling cache"); |
1816 | 1849 | cacheState = CacheState.DISABLED; |
1817 | 1850 | ioEngine.shutdown(); |
1818 | 1851 | 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(); |
1828 | 1854 | } |
| 1855 | + ramCache.clear(); |
1829 | 1856 | if (cacheStats.getMetricsRollerScheduler() != null) { |
1830 | 1857 | cacheStats.getMetricsRollerScheduler().shutdownNow(); |
1831 | 1858 | } |
| 1859 | + cacheCleanupThread = |
| 1860 | + Threads.setDaemonThreadRunning(new Thread(() -> cleanupCache(persistOnCleanup)), |
| 1861 | + "BucketCacheCleanup-" + System.identityHashCode(this), Threads.LOGGING_EXCEPTION_HANDLER); |
1832 | 1862 | } |
1833 | 1863 |
|
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()) { |
1846 | 1869 | 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 | 1870 | persistToFile(); |
1856 | 1871 | } catch (IOException ex) { |
1857 | 1872 | 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); |
1860 | 1873 | } |
1861 | 1874 | } |
| 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(); |
1862 | 1925 | } |
1863 | 1926 | } |
1864 | 1927 |
|
| 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 | + |
1865 | 1936 | /** |
1866 | 1937 | * Needed mostly for UTs that might run in the same VM and create different BucketCache instances |
1867 | 1938 | * on different UT methods. |
|
0 commit comments