001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.io.hfile.bucket;
019
020import static org.apache.hadoop.hbase.io.hfile.CacheConfig.BUCKETCACHE_PERSIST_INTERVAL_KEY;
021import static org.apache.hadoop.hbase.io.hfile.bucket.BucketCache.BACKING_MAP_PERSISTENCE_CHUNK_SIZE;
022import static org.apache.hadoop.hbase.io.hfile.bucket.BucketCache.DEFAULT_ERROR_TOLERATION_DURATION;
023import static org.junit.jupiter.api.Assertions.assertEquals;
024import static org.junit.jupiter.api.Assertions.assertNotEquals;
025import static org.junit.jupiter.api.Assertions.assertNull;
026import static org.junit.jupiter.api.Assertions.assertTrue;
027
028import java.io.BufferedWriter;
029import java.io.File;
030import java.io.FileOutputStream;
031import java.io.OutputStreamWriter;
032import java.io.RandomAccessFile;
033import java.nio.file.FileSystems;
034import java.nio.file.Files;
035import java.nio.file.attribute.FileTime;
036import java.time.Instant;
037import java.util.stream.Stream;
038import org.apache.hadoop.conf.Configuration;
039import org.apache.hadoop.fs.Path;
040import org.apache.hadoop.hbase.HBaseConfiguration;
041import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
042import org.apache.hadoop.hbase.HBaseTestingUtil;
043import org.apache.hadoop.hbase.Waiter;
044import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
045import org.apache.hadoop.hbase.io.hfile.BlockType;
046import org.apache.hadoop.hbase.io.hfile.CacheConfig;
047import org.apache.hadoop.hbase.io.hfile.CacheTestUtils;
048import org.apache.hadoop.hbase.io.hfile.Cacheable;
049import org.apache.hadoop.hbase.testclassification.RegionServerTests;
050import org.apache.hadoop.hbase.testclassification.SmallTests;
051import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
052import org.junit.jupiter.api.Tag;
053import org.junit.jupiter.api.TestTemplate;
054import org.junit.jupiter.params.provider.Arguments;
055
056/**
057 * Basic test for check file's integrity before start BucketCache in fileIOEngine
058 */
059@Tag(SmallTests.TAG)
060@Tag(RegionServerTests.TAG)
061@HBaseParameterizedTestTemplate(name = "{index}: blockSize={0}, bucketSizes={1}")
062public class TestVerifyBucketCacheFile {
063
064  public static Stream<Arguments> parameters() {
065    return Stream.of(Arguments.of(8192, null),
066      Arguments.of(16 * 1024,
067        new int[] { 2 * 1024 + 1024, 4 * 1024 + 1024, 8 * 1024 + 1024, 16 * 1024 + 1024,
068          28 * 1024 + 1024, 32 * 1024 + 1024, 64 * 1024 + 1024, 96 * 1024 + 1024,
069          128 * 1024 + 1024 }));
070  }
071
072  private final int constructedBlockSize;
073  private final int[] constructedBlockSizes;
074
075  public TestVerifyBucketCacheFile(int constructedBlockSize, int[] constructedBlockSizes) {
076    this.constructedBlockSize = constructedBlockSize;
077    this.constructedBlockSizes = constructedBlockSizes;
078  }
079
080  final long capacitySize = 32 * 1024 * 1024;
081  final int writeThreads = BucketCache.DEFAULT_WRITER_THREADS;
082  final int writerQLen = BucketCache.DEFAULT_WRITER_QUEUE_ITEMS;
083
084  /**
085   * Test cache file or persistence file does not exist whether BucketCache starts normally (1)
086   * Start BucketCache and add some blocks, then shutdown BucketCache and persist cache to file.
087   * Restart BucketCache and it can restore cache from file. (2) Delete bucket cache file after
088   * shutdown BucketCache. Restart BucketCache and it can't restore cache from file, the cache file
089   * and persistence file would be deleted before BucketCache start normally. (3) Delete persistence
090   * file after shutdown BucketCache. Restart BucketCache and it can't restore cache from file, the
091   * cache file and persistence file would be deleted before BucketCache start normally.
092   * @throws Exception the exception
093   */
094  @TestTemplate
095  public void testRetrieveFromFile() throws Exception {
096    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
097    Path testDir = TEST_UTIL.getDataTestDir();
098    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
099
100    Configuration conf = HBaseConfiguration.create();
101    // Disables the persister thread by setting its interval to MAX_VALUE
102    conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
103    BucketCache bucketCache = null;
104    BucketCache recoveredBucketCache = null;
105    try {
106      bucketCache =
107        new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
108          constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
109      assertTrue(bucketCache.waitForCacheInitialization(10000));
110      long usedSize = bucketCache.getAllocator().getUsedSize();
111      assertEquals(0, usedSize);
112      CacheTestUtils.HFileBlockPair[] blocks =
113        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
114      String[] names = CacheTestUtils.getHFileNames(blocks);
115      // Add blocks
116      for (CacheTestUtils.HFileBlockPair block : blocks) {
117        cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
118      }
119      usedSize = bucketCache.getAllocator().getUsedSize();
120      assertNotEquals(0, usedSize);
121      // 1.persist cache to file
122      bucketCache.shutdown();
123      // restore cache from file
124      bucketCache =
125        new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
126          constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
127      assertTrue(bucketCache.waitForCacheInitialization(10000));
128      assertEquals(usedSize, bucketCache.getAllocator().getUsedSize());
129      // persist cache to file
130      bucketCache.shutdown();
131
132      // 2.delete bucket cache file
133      final java.nio.file.Path cacheFile =
134        FileSystems.getDefault().getPath(testDir.toString(), "bucket.cache");
135      assertTrue(Files.deleteIfExists(cacheFile));
136      // can't restore cache from file
137      recoveredBucketCache =
138        new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, constructedBlockSize,
139          constructedBlockSizes, writeThreads, writerQLen, testDir + "/bucket.persistence");
140      assertTrue(recoveredBucketCache.waitForCacheInitialization(10000));
141      waitPersistentCacheValidation(conf, recoveredBucketCache);
142      assertEquals(0, recoveredBucketCache.getAllocator().getUsedSize());
143      assertEquals(0, recoveredBucketCache.backingMap.size());
144      BlockCacheKey[] newKeys = CacheTestUtils.regenerateKeys(blocks, names);
145      // Add blocks
146      for (int i = 0; i < blocks.length; i++) {
147        cacheAndWaitUntilFlushedToBucket(recoveredBucketCache, newKeys[i], blocks[i].getBlock());
148      }
149      usedSize = recoveredBucketCache.getAllocator().getUsedSize();
150      assertNotEquals(0, usedSize);
151      // persist cache to file
152      recoveredBucketCache.shutdown();
153
154      // 3.delete backingMap persistence file
155      final java.nio.file.Path mapFile =
156        FileSystems.getDefault().getPath(testDir.toString(), "bucket.persistence");
157      assertTrue(Files.deleteIfExists(mapFile));
158      // can't restore cache from file
159      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
160        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
161        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
162      assertTrue(bucketCache.waitForCacheInitialization(10000));
163      waitPersistentCacheValidation(conf, bucketCache);
164      assertEquals(0, bucketCache.getAllocator().getUsedSize());
165      assertEquals(0, bucketCache.backingMap.size());
166    } finally {
167      if (recoveredBucketCache == null && bucketCache != null) {
168        bucketCache.shutdown();
169      }
170      if (recoveredBucketCache != null) {
171        recoveredBucketCache.shutdown();
172      }
173    }
174    TEST_UTIL.cleanupTestDir();
175  }
176
177  @TestTemplate
178  public void testRetrieveFromFileAfterDelete() throws Exception {
179    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
180    Path testDir = TEST_UTIL.getDataTestDir();
181    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
182    Configuration conf = TEST_UTIL.getConfiguration();
183    conf.setLong(CacheConfig.BUCKETCACHE_PERSIST_INTERVAL_KEY, 300);
184    String mapFileName = testDir + "/bucket.persistence" + EnvironmentEdgeManager.currentTime();
185    BucketCache bucketCache = null;
186    try {
187      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
188        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
189        DEFAULT_ERROR_TOLERATION_DURATION, conf);
190      assertTrue(bucketCache.waitForCacheInitialization(10000));
191      long usedSize = bucketCache.getAllocator().getUsedSize();
192      assertEquals(0, usedSize);
193      CacheTestUtils.HFileBlockPair[] blocks =
194        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
195      // Add blocks
196      for (CacheTestUtils.HFileBlockPair block : blocks) {
197        cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
198      }
199      usedSize = bucketCache.getAllocator().getUsedSize();
200      assertNotEquals(0, usedSize);
201      // Shutdown BucketCache
202      bucketCache.shutdown();
203      // Delete the persistence file
204      File mapFile = new File(mapFileName);
205      assertTrue(mapFile.delete());
206      Thread.sleep(350);
207      // Create BucketCache
208      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
209        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
210        DEFAULT_ERROR_TOLERATION_DURATION, conf);
211      assertTrue(bucketCache.waitForCacheInitialization(10000));
212      waitPersistentCacheValidation(conf, bucketCache);
213      assertEquals(0, bucketCache.getAllocator().getUsedSize());
214      assertEquals(0, bucketCache.backingMap.size());
215    } finally {
216      if (bucketCache != null) {
217        bucketCache.shutdown();
218      }
219    }
220  }
221
222  /**
223   * Test whether BucketCache is started normally after modifying the cache file. Start BucketCache
224   * and add some blocks, then shutdown BucketCache and persist cache to file. Restart BucketCache
225   * after modify cache file's data, and it can't restore cache from file, the cache file and
226   * persistence file would be deleted before BucketCache start normally.
227   * @throws Exception the exception
228   */
229  @TestTemplate
230  public void testModifiedBucketCacheFileData() throws Exception {
231    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
232    Path testDir = TEST_UTIL.getDataTestDir();
233    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
234
235    Configuration conf = HBaseConfiguration.create();
236    // Disables the persister thread by setting its interval to MAX_VALUE
237    conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
238    BucketCache bucketCache = null;
239    try {
240      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
241        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
242        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
243      assertTrue(bucketCache.waitForCacheInitialization(10000));
244      long usedSize = bucketCache.getAllocator().getUsedSize();
245      assertEquals(0, usedSize);
246
247      CacheTestUtils.HFileBlockPair[] blocks =
248        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
249      // Add blocks
250      for (CacheTestUtils.HFileBlockPair block : blocks) {
251        cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
252      }
253      usedSize = bucketCache.getAllocator().getUsedSize();
254      assertNotEquals(0, usedSize);
255      // persist cache to file
256      bucketCache.shutdown();
257
258      // modified bucket cache file
259      String file = testDir + "/bucket.cache";
260      try (BufferedWriter out =
261        new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false)))) {
262        out.write("test bucket cache");
263      }
264      // can't restore cache from file
265      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
266        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
267        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
268      assertTrue(bucketCache.waitForCacheInitialization(10000));
269      waitPersistentCacheValidation(conf, bucketCache);
270      assertEquals(0, bucketCache.getAllocator().getUsedSize());
271      assertEquals(0, bucketCache.backingMap.size());
272    } finally {
273      if (bucketCache != null) {
274        bucketCache.shutdown();
275      }
276    }
277    TEST_UTIL.cleanupTestDir();
278  }
279
280  /**
281   * Test whether BucketCache is started normally after modifying the cache file's last modified
282   * time. First Start BucketCache and add some blocks, then shutdown BucketCache and persist cache
283   * to file. Then Restart BucketCache after modify cache file's last modified time. HBASE-XXXX has
284   * modified persistence cache such that now we store extra 8 bytes at the end of each block in the
285   * cache, representing the nanosecond time the block has been cached. So in the event the cache
286   * file has failed checksum verification during loading time, we go through all the cached blocks
287   * in the cache map and validate the cached time long between what is in the map and the cache
288   * file. If that check fails, we pull the cache key entry out of the map. Since in this test we
289   * are only modifying the access time to induce a checksum error, the cache file content is still
290   * valid and the extra verification should validate that all cache keys in the map are still
291   * recoverable from the cache.
292   * @throws Exception the exception
293   */
294  @TestTemplate
295  public void testModifiedBucketCacheFileTime() throws Exception {
296    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
297    Path testDir = TEST_UTIL.getDataTestDir();
298    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
299    Configuration conf = HBaseConfiguration.create();
300    // Disables the persister thread by setting its interval to MAX_VALUE
301    conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
302    BucketCache bucketCache = null;
303    try {
304      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
305        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
306        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
307      assertTrue(bucketCache.waitForCacheInitialization(10000));
308      long usedSize = bucketCache.getAllocator().getUsedSize();
309      assertEquals(0, usedSize);
310
311      CacheTestUtils.HFileBlockPair[] blocks =
312        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 1);
313      // Add blocks
314      for (CacheTestUtils.HFileBlockPair block : blocks) {
315        cacheAndWaitUntilFlushedToBucket(bucketCache, block.getBlockName(), block.getBlock());
316      }
317      usedSize = bucketCache.getAllocator().getUsedSize();
318      assertNotEquals(0, usedSize);
319      long blockCount = bucketCache.backingMap.size();
320      assertNotEquals(0, blockCount);
321      // persist cache to file
322      bucketCache.shutdown();
323
324      // modified bucket cache file LastModifiedTime
325      final java.nio.file.Path file =
326        FileSystems.getDefault().getPath(testDir.toString(), "bucket.cache");
327      Files.setLastModifiedTime(file, FileTime.from(Instant.now().plusMillis(1_000)));
328      // can't restore cache from file
329      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
330        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
331        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
332      assertTrue(bucketCache.waitForCacheInitialization(10000));
333      waitPersistentCacheValidation(conf, bucketCache);
334      assertEquals(usedSize, bucketCache.getAllocator().getUsedSize());
335      assertEquals(blockCount, bucketCache.backingMap.size());
336    } finally {
337      if (bucketCache != null) {
338        bucketCache.shutdown();
339      }
340    }
341    TEST_UTIL.cleanupTestDir();
342  }
343
344  /**
345   * Test whether the region cached size is still correct after the cache validation thread evicts
346   * an invalid block. First start BucketCache and add three blocks for the same region, then
347   * shutdown BucketCache and persist cache to file. Then overwrite the cached time recorded in the
348   * first block and modify the cache file's last modified time, so that the checksum verification
349   * fails when restarting BucketCache. The validation thread then goes through all the cached
350   * blocks and evicts the first one, which should be discounted from the region cached size only
351   * once.
352   * @throws Exception the exception
353   */
354  @TestTemplate
355  public void testRegionCachedSizeAfterInvalidBlockEvictedOnValidation() throws Exception {
356    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
357    Path testDir = TEST_UTIL.getDataTestDir();
358    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
359    Configuration conf = HBaseConfiguration.create();
360    // Disables the persister thread by setting its interval to MAX_VALUE
361    conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
362    String regionName = "region";
363    BucketCache bucketCache = null;
364    try {
365      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
366        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
367        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
368      assertTrue(bucketCache.waitForCacheInitialization(10000));
369
370      CacheTestUtils.HFileBlockPair[] blocks =
371        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 3);
372      // Add three blocks, all belonging to the same region
373      BlockCacheKey[] keys = new BlockCacheKey[blocks.length];
374      for (int i = 0; i < blocks.length; i++) {
375        keys[i] = new BlockCacheKey("hfile", "cf", regionName, (long) i * constructedBlockSize,
376          true, BlockType.DATA, false);
377        cacheAndWaitUntilFlushedToBucket(bucketCache, keys[i], blocks[i].getBlock());
378      }
379      long firstBlockOffset = bucketCache.backingMap.get(keys[0]).offset();
380      // persist cache to file
381      bucketCache.shutdown();
382
383      // overwrite the cached time stored at the start of the first block
384      try (RandomAccessFile raf = new RandomAccessFile(testDir + "/bucket.cache", "rw")) {
385        raf.seek(firstBlockOffset);
386        raf.writeLong(-1L);
387      }
388      // modified bucket cache file LastModifiedTime, so the checksum verification fails
389      final java.nio.file.Path file =
390        FileSystems.getDefault().getPath(testDir.toString(), "bucket.cache");
391      Files.setLastModifiedTime(file, FileTime.from(Instant.now().plusMillis(1_000)));
392
393      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
394        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen,
395        testDir + "/bucket.persistence", DEFAULT_ERROR_TOLERATION_DURATION, conf);
396      assertTrue(bucketCache.waitForCacheInitialization(10000));
397      waitPersistentCacheValidation(conf, bucketCache);
398
399      // The region cached size should match the blocks still in the cache. We sum what is left
400      // instead of expecting two blocks, as the validation thread may not evict the first block
401      // if it checks it before the cache is enabled.
402      long remainingSize = 0;
403      for (BlockCacheKey key : keys) {
404        BucketEntry entry = bucketCache.backingMap.get(key);
405        if (entry != null) {
406          remainingSize += entry.getLength();
407        }
408      }
409      assertNotEquals(0, remainingSize);
410      assertEquals(remainingSize,
411        (long) bucketCache.getRegionCachedInfo().get().getOrDefault(regionName, 0L));
412    } finally {
413      if (bucketCache != null) {
414        bucketCache.shutdown();
415      }
416    }
417    TEST_UTIL.cleanupTestDir();
418  }
419
420  /**
421   * When using persistent bucket cache, there may be crashes between persisting the backing map and
422   * syncing new blocks to the cache file itself, leading to an inconsistent state between the cache
423   * keys and the cached data. This is to make sure the cache keys are updated accordingly, and the
424   * keys that are still valid do succeed in retrieve related block data from the cache without any
425   * corruption.
426   * @throws Exception the exception
427   */
428  @TestTemplate
429  public void testBucketCacheRecovery() throws Exception {
430    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
431    Path testDir = TEST_UTIL.getDataTestDir();
432    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
433    Configuration conf = HBaseConfiguration.create();
434    // Disables the persister thread by setting its interval to MAX_VALUE
435    conf.setLong(BUCKETCACHE_PERSIST_INTERVAL_KEY, Long.MAX_VALUE);
436    String mapFileName = testDir + "/bucket.persistence" + EnvironmentEdgeManager.currentTime();
437    BucketCache bucketCache = null;
438    BucketCache newBucketCache = null;
439    try {
440      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
441        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
442        DEFAULT_ERROR_TOLERATION_DURATION, conf);
443      assertTrue(bucketCache.waitForCacheInitialization(10000));
444
445      CacheTestUtils.HFileBlockPair[] blocks =
446        CacheTestUtils.generateHFileBlocks(constructedBlockSize, 4);
447      String[] names = CacheTestUtils.getHFileNames(blocks);
448      // Add three blocks
449      cacheAndWaitUntilFlushedToBucket(bucketCache, blocks[0].getBlockName(), blocks[0].getBlock());
450      cacheAndWaitUntilFlushedToBucket(bucketCache, blocks[1].getBlockName(), blocks[1].getBlock());
451      cacheAndWaitUntilFlushedToBucket(bucketCache, blocks[2].getBlockName(), blocks[2].getBlock());
452      // saves the current state
453      bucketCache.persistToFile();
454      // evicts first block
455      bucketCache.evictBlock(blocks[0].getBlockName());
456
457      // now adds a fourth block to bucket cache
458      cacheAndWaitUntilFlushedToBucket(bucketCache, blocks[3].getBlockName(), blocks[3].getBlock());
459      // Creates new bucket cache instance without persisting to file after evicting first block
460      // and caching fourth block. So the bucket cache file has only the last three blocks,
461      // but backing map (containing cache keys) was persisted when first three blocks
462      // were in the cache. So the state on this recovery is:
463      // - Backing map: [block0, block1, block2]
464      // - Cache: [block1, block2, block3]
465      // Therefore, this bucket cache would be able to recover only block1 and block2.
466      newBucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
467        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
468        DEFAULT_ERROR_TOLERATION_DURATION, conf);
469      assertTrue(newBucketCache.waitForCacheInitialization(10000));
470      BlockCacheKey[] newKeys = CacheTestUtils.regenerateKeys(blocks, names);
471      assertNull(newBucketCache.getBlock(newKeys[0], false, false, false));
472      assertEquals(blocks[1].getBlock(), newBucketCache.getBlock(newKeys[1], false, false, false));
473      assertEquals(blocks[2].getBlock(), newBucketCache.getBlock(newKeys[2], false, false, false));
474      assertNull(newBucketCache.getBlock(newKeys[3], false, false, false));
475      assertEquals(2, newBucketCache.backingMap.size());
476    } finally {
477      if (newBucketCache == null && bucketCache != null) {
478        bucketCache.shutdown();
479      }
480      if (newBucketCache != null) {
481        newBucketCache.shutdown();
482      }
483      TEST_UTIL.cleanupTestDir();
484    }
485  }
486
487  @TestTemplate
488  public void testSingleChunk() throws Exception {
489    testChunkedBackingMapRecovery(5, 5);
490  }
491
492  @TestTemplate
493  public void testCompletelyFilledChunks() throws Exception {
494    // Test where the all the chunks are complete with chunkSize entries
495    testChunkedBackingMapRecovery(5, 10);
496  }
497
498  @TestTemplate
499  public void testPartiallyFilledChunks() throws Exception {
500    // Test where the last chunk is not completely filled.
501    testChunkedBackingMapRecovery(5, 13);
502  }
503
504  private void testChunkedBackingMapRecovery(int chunkSize, int numBlocks) throws Exception {
505    HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
506    Path testDir = TEST_UTIL.getDataTestDir();
507    TEST_UTIL.getTestFileSystem().mkdirs(testDir);
508    Configuration conf = HBaseConfiguration.create();
509    conf.setLong(BACKING_MAP_PERSISTENCE_CHUNK_SIZE, chunkSize);
510
511    String mapFileName = testDir + "/bucket.persistence" + EnvironmentEdgeManager.currentTime();
512    BucketCache bucketCache = null;
513    BucketCache newBucketCache = null;
514    try {
515      bucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
516        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
517        DEFAULT_ERROR_TOLERATION_DURATION, conf);
518      assertTrue(bucketCache.waitForCacheInitialization(10000));
519
520      CacheTestUtils.HFileBlockPair[] blocks =
521        CacheTestUtils.generateHFileBlocks(constructedBlockSize, numBlocks);
522      String[] names = CacheTestUtils.getHFileNames(blocks);
523
524      for (int i = 0; i < numBlocks; i++) {
525        cacheAndWaitUntilFlushedToBucket(bucketCache, blocks[i].getBlockName(),
526          blocks[i].getBlock());
527      }
528
529      // saves the current state
530      bucketCache.persistToFile();
531
532      // Create a new bucket which reads from persistence file.
533      newBucketCache = new BucketCache("file:" + testDir + "/bucket.cache", capacitySize,
534        constructedBlockSize, constructedBlockSizes, writeThreads, writerQLen, mapFileName,
535        DEFAULT_ERROR_TOLERATION_DURATION, conf);
536      assertTrue(newBucketCache.waitForCacheInitialization(10000));
537
538      assertEquals(numBlocks, newBucketCache.backingMap.size());
539      BlockCacheKey[] newKeys = CacheTestUtils.regenerateKeys(blocks, names);
540      for (int i = 0; i < numBlocks; i++) {
541        assertEquals(blocks[i].getBlock(),
542          newBucketCache.getBlock(newKeys[i], false, false, false));
543      }
544    } finally {
545      if (newBucketCache == null && bucketCache != null) {
546        bucketCache.shutdown();
547      }
548      if (newBucketCache != null) {
549        newBucketCache.shutdown();
550      }
551      TEST_UTIL.cleanupTestDir();
552    }
553  }
554
555  private void waitUntilFlushedToBucket(BucketCache cache, BlockCacheKey cacheKey)
556    throws InterruptedException {
557    while (!cache.backingMap.containsKey(cacheKey) || cache.ramCache.containsKey(cacheKey)) {
558      Thread.sleep(100);
559    }
560  }
561
562  // BucketCache.cacheBlock is async, it first adds block to ramCache and writeQueue, then writer
563  // threads will flush it to the bucket and put reference entry in backingMap.
564  private void cacheAndWaitUntilFlushedToBucket(BucketCache cache, BlockCacheKey cacheKey,
565    Cacheable block) throws InterruptedException {
566    cache.cacheBlock(cacheKey, block);
567    waitUntilFlushedToBucket(cache, cacheKey);
568  }
569
570  private void waitPersistentCacheValidation(Configuration config, final BucketCache bucketCache) {
571    Waiter.waitFor(config, 5000,
572      () -> bucketCache.getBackingMapValidated().get() && bucketCache.isCacheEnabled());
573  }
574}