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.regionserver;
019
020import static org.apache.hadoop.hbase.HConstants.BUCKET_CACHE_SIZE_KEY;
021import static org.apache.hadoop.hbase.io.hfile.bucket.BucketCache.DEFAULT_ERROR_TOLERATION_DURATION;
022import static org.junit.Assert.assertEquals;
023import static org.junit.Assert.assertFalse;
024import static org.junit.Assert.assertNotNull;
025import static org.junit.Assert.assertNull;
026import static org.junit.Assert.assertTrue;
027import static org.junit.Assert.fail;
028
029import java.io.IOException;
030import java.util.ArrayList;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Optional;
036import java.util.Random;
037import java.util.Set;
038import org.apache.hadoop.conf.Configuration;
039import org.apache.hadoop.fs.FileSystem;
040import org.apache.hadoop.fs.Path;
041import org.apache.hadoop.hbase.HBaseClassTestRule;
042import org.apache.hadoop.hbase.HBaseTestingUtil;
043import org.apache.hadoop.hbase.HConstants;
044import org.apache.hadoop.hbase.KeyValue;
045import org.apache.hadoop.hbase.TableName;
046import org.apache.hadoop.hbase.Waiter;
047import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
048import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
049import org.apache.hadoop.hbase.client.RegionInfo;
050import org.apache.hadoop.hbase.client.RegionInfoBuilder;
051import org.apache.hadoop.hbase.client.TableDescriptor;
052import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
053import org.apache.hadoop.hbase.fs.HFileSystem;
054import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
055import org.apache.hadoop.hbase.io.hfile.BlockCache;
056import org.apache.hadoop.hbase.io.hfile.BlockCacheFactory;
057import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
058import org.apache.hadoop.hbase.io.hfile.BlockType;
059import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory;
060import org.apache.hadoop.hbase.io.hfile.CacheConfig;
061import org.apache.hadoop.hbase.io.hfile.CacheTestUtils;
062import org.apache.hadoop.hbase.io.hfile.HFileBlock;
063import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
064import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache;
065import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker;
066import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory;
067import org.apache.hadoop.hbase.testclassification.RegionServerTests;
068import org.apache.hadoop.hbase.testclassification.SmallTests;
069import org.apache.hadoop.hbase.util.Bytes;
070import org.apache.hadoop.hbase.util.CommonFSUtils;
071import org.apache.hadoop.hbase.util.Pair;
072import org.junit.BeforeClass;
073import org.junit.ClassRule;
074import org.junit.Test;
075import org.junit.experimental.categories.Category;
076import org.slf4j.Logger;
077import org.slf4j.LoggerFactory;
078
079/**
080 * This class is used to test the functionality of the DataTieringManager.
081 *
082 * The mock online regions are stored in {@link TestCustomCellDataTieringManager#testOnlineRegions}.
083 * For all tests, the setup of
084 *  {@link TestCustomCellDataTieringManager#testOnlineRegions} occurs only once.
085 * Please refer to {@link TestCustomCellDataTieringManager#setupOnlineRegions()} for the structure.
086 * Additionally, a list of all store files is
087 *  maintained in {@link TestCustomCellDataTieringManager#hStoreFiles}.
088 * The characteristics of these store files are listed below:
089 * @formatter:off
090 * ## HStoreFile Information
091 * | HStoreFile       | Region             | Store               | DataTiering           | isHot |
092 * |------------------|--------------------|---------------------|-----------------------|-------|
093 * | hStoreFile0      | region1            | hStore11            | CUSTOM_CELL_VALUE     | true  |
094 * | hStoreFile1      | region1            | hStore12            | NONE                  | true  |
095 * | hStoreFile2      | region2            | hStore21            | CUSTOM_CELL_VALUE     | true  |
096 * | hStoreFile3      | region2            | hStore22            | CUSTOM_CELL_VALUE     | false |
097 * @formatter:on
098 */
099
100@Category({ RegionServerTests.class, SmallTests.class })
101public class TestCustomCellDataTieringManager {
102
103  @ClassRule
104  public static final HBaseClassTestRule CLASS_RULE =
105    HBaseClassTestRule.forClass(TestCustomCellDataTieringManager.class);
106
107  private static final Logger LOG = LoggerFactory.getLogger(TestCustomCellDataTieringManager.class);
108  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
109  private static final long DAY = 24 * 60 * 60 * 1000;
110  private static Configuration defaultConf;
111  private static FileSystem fs;
112  private BlockCache blockCache;
113  private static CacheConfig cacheConf;
114  private static Path testDir;
115  private static final Map<String, HRegion> testOnlineRegions = new HashMap<>();
116
117  private static DataTieringManager dataTieringManager;
118  private static final List<HStoreFile> hStoreFiles = new ArrayList<>();
119
120  /**
121   * Represents the current lexicographically increasing string used as a row key when writing
122   * HFiles. It is incremented each time {@link #nextString()} is called to generate unique row
123   * keys.
124   */
125  private static String rowKeyString;
126
127  @BeforeClass
128  public static void setupBeforeClass() throws Exception {
129    testDir = TEST_UTIL.getDataTestDir(TestCustomCellDataTieringManager.class.getSimpleName());
130    defaultConf = TEST_UTIL.getConfiguration();
131    updateCommonConfigurations();
132    DataTieringManager.instantiate(defaultConf, testOnlineRegions);
133    dataTieringManager = DataTieringManager.getInstance();
134    rowKeyString = "";
135  }
136
137  private static void updateCommonConfigurations() {
138    defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, true);
139    defaultConf.setStrings(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap");
140    defaultConf.setLong(BUCKET_CACHE_SIZE_KEY, 32);
141  }
142
143  @FunctionalInterface
144  interface DataTieringMethodCallerWithPath {
145    boolean call(DataTieringManager manager, Path path) throws DataTieringException;
146  }
147
148  @FunctionalInterface
149  interface DataTieringMethodCallerWithKey {
150    boolean call(DataTieringManager manager, BlockCacheKey key) throws DataTieringException;
151  }
152
153  @Test
154  public void testDataTieringEnabledWithKey() throws IOException {
155    initializeTestEnvironment();
156    DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isDataTieringEnabled;
157
158    // Test with valid key
159    BlockCacheKey key = new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA);
160    testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, true);
161
162    // Test with another valid key
163    key = new BlockCacheKey(hStoreFiles.get(1).getPath(), 0, true, BlockType.DATA);
164    testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, false);
165
166  }
167
168  @Test
169  public void testDataTieringEnabledWithPath() throws IOException {
170    initializeTestEnvironment();
171    DataTieringMethodCallerWithPath methodCallerWithPath = DataTieringManager::isDataTieringEnabled;
172
173    // Test with valid path
174    Path hFilePath = hStoreFiles.get(1).getPath();
175    testDataTieringMethodWithPathNoException(methodCallerWithPath, hFilePath, false);
176
177    // Test with another valid path
178    hFilePath = hStoreFiles.get(3).getPath();
179    testDataTieringMethodWithPathNoException(methodCallerWithPath, hFilePath, true);
180
181    // Test with an incorrect path
182    hFilePath = new Path("incorrectPath");
183    testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath,
184      new DataTieringException("Incorrect HFile Path: " + hFilePath));
185
186    // Test with a non-existing HRegion path
187    Path basePath = hStoreFiles.get(0).getPath().getParent().getParent().getParent();
188    hFilePath = new Path(basePath, "incorrectRegion/cf1/filename");
189    testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath,
190      new DataTieringException("HRegion corresponding to incorrectRegion doesn't exist"));
191
192    // Test with a non-existing HStore path
193    basePath = hStoreFiles.get(0).getPath().getParent().getParent();
194    hFilePath = new Path(basePath, "incorrectCf/filename");
195    testDataTieringMethodWithPathExpectingException(methodCallerWithPath, hFilePath,
196      new DataTieringException(
197        "HStore corresponding to " + basePath.getName() + "/incorrectCf doesn't exist"));
198  }
199
200  @Test
201  public void testHotDataWithKey() throws IOException {
202    initializeTestEnvironment();
203    DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isHotData;
204
205    // Test with valid key
206    BlockCacheKey key = new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA);
207    testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, true);
208
209    // Test with another valid key
210    key = new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA);
211    testDataTieringMethodWithKeyNoException(methodCallerWithKey, key, false);
212  }
213
214  @Test
215  public void testPrefetchWhenDataTieringEnabled() throws IOException {
216    setPrefetchBlocksOnOpen();
217    this.blockCache = initializeTestEnvironment();
218    // Evict blocks from cache by closing the files and passing evict on close.
219    // Then initialize the reader again. Since Prefetch on open is set to true, it should prefetch
220    // those blocks.
221    for (HStoreFile file : hStoreFiles) {
222      file.closeStoreFile(true);
223      file.initReader();
224    }
225
226    // Since we have one cold file among four files, only three should get prefetched.
227    Optional<Map<String, Pair<String, Long>>> fullyCachedFiles = blockCache.getFullyCachedFiles();
228    assertTrue("We should get the fully cached files from the cache", fullyCachedFiles.isPresent());
229    Waiter.waitFor(defaultConf, 10000, () -> fullyCachedFiles.get().size() == 3);
230    assertEquals("Number of fully cached files are incorrect", 3, fullyCachedFiles.get().size());
231  }
232
233  private void setPrefetchBlocksOnOpen() {
234    defaultConf.setBoolean(CacheConfig.PREFETCH_BLOCKS_ON_OPEN_KEY, true);
235  }
236
237  @Test
238  public void testColdDataFiles() throws IOException {
239    initializeTestEnvironment();
240    Set<BlockCacheKey> allCachedBlocks = new HashSet<>();
241    for (HStoreFile file : hStoreFiles) {
242      allCachedBlocks.add(new BlockCacheKey(file.getPath(), 0, true, BlockType.DATA));
243    }
244
245    // Verify hStoreFile3 is identified as cold data
246    DataTieringMethodCallerWithKey methodCallerWithKey = DataTieringManager::isHotData;
247    Path hFilePath = hStoreFiles.get(3).getPath();
248    testDataTieringMethodWithKeyNoException(methodCallerWithKey,
249      new BlockCacheKey(hFilePath, 0, true, BlockType.DATA), false);
250
251    // Verify all the other files in hStoreFiles are hot data
252    for (int i = 0; i < hStoreFiles.size() - 1; i++) {
253      hFilePath = hStoreFiles.get(i).getPath();
254      testDataTieringMethodWithKeyNoException(methodCallerWithKey,
255        new BlockCacheKey(hFilePath, 0, true, BlockType.DATA), true);
256    }
257
258    try {
259      Set<String> coldFilePaths = dataTieringManager.getColdDataFiles(allCachedBlocks);
260      assertEquals(1, coldFilePaths.size());
261    } catch (DataTieringException e) {
262      fail("Unexpected DataTieringException: " + e.getMessage());
263    }
264  }
265
266  @Test
267  public void testCacheCompactedBlocksOnWriteDataTieringDisabled() throws IOException {
268    setCacheCompactBlocksOnWrite();
269    this.blockCache = initializeTestEnvironment();
270    HRegion region = createHRegion("table3", this.blockCache);
271    testCacheCompactedBlocksOnWrite(region, true);
272  }
273
274  @Test
275  public void testCacheCompactedBlocksOnWriteWithHotData() throws IOException {
276    setCacheCompactBlocksOnWrite();
277    this.blockCache = initializeTestEnvironment();
278    HRegion region =
279      createHRegion("table3", getConfWithCustomCellDataTieringEnabled(5 * DAY), this.blockCache);
280    testCacheCompactedBlocksOnWrite(region, true);
281  }
282
283  @Test
284  public void testCacheCompactedBlocksOnWriteWithColdData() throws IOException {
285    setCacheCompactBlocksOnWrite();
286    this.blockCache = initializeTestEnvironment();
287    HRegion region =
288      createHRegion("table3", getConfWithCustomCellDataTieringEnabled(DAY), this.blockCache);
289    testCacheCompactedBlocksOnWrite(region, false);
290  }
291
292  private void setCacheCompactBlocksOnWrite() {
293    defaultConf.setBoolean(CacheConfig.CACHE_COMPACTED_BLOCKS_ON_WRITE_KEY, true);
294  }
295
296  private void testCacheCompactedBlocksOnWrite(HRegion region, boolean expectDataBlocksCached)
297    throws IOException {
298    HStore hStore = createHStore(region, "cf1");
299    createTestFilesForCompaction(hStore);
300    hStore.refreshStoreFiles();
301
302    region.stores.put(Bytes.toBytes("cf1"), hStore);
303    testOnlineRegions.put(region.getRegionInfo().getEncodedName(), region);
304
305    long initialStoreFilesCount = hStore.getStorefilesCount();
306    long initialCacheDataBlockCount = blockCache.getDataBlockCount();
307    assertEquals(3, initialStoreFilesCount);
308    assertEquals(0, initialCacheDataBlockCount);
309
310    region.compact(true);
311
312    long compactedStoreFilesCount = hStore.getStorefilesCount();
313    long compactedCacheDataBlockCount = blockCache.getDataBlockCount();
314    assertEquals(1, compactedStoreFilesCount);
315    assertEquals(expectDataBlocksCached, compactedCacheDataBlockCount > 0);
316  }
317
318  private void createTestFilesForCompaction(HStore hStore) throws IOException {
319    long currentTime = System.currentTimeMillis();
320    Path storeDir = hStore.getStoreContext().getFamilyStoreDirectoryPath();
321    Configuration configuration = hStore.getReadOnlyConfiguration();
322
323    HRegionFileSystem regionFS = hStore.getHRegion().getRegionFileSystem();
324
325    createHStoreFile(storeDir, configuration, currentTime - 2 * DAY, regionFS);
326    createHStoreFile(storeDir, configuration, currentTime - 3 * DAY, regionFS);
327    createHStoreFile(storeDir, configuration, currentTime - 4 * DAY, regionFS);
328  }
329
330  @Test
331  public void testPickColdDataFiles() throws IOException {
332    initializeTestEnvironment();
333    Map<String, String> coldDataFiles = dataTieringManager.getColdFilesList();
334    assertEquals(1, coldDataFiles.size());
335    // hStoreFiles[3] is the cold file.
336    assert (coldDataFiles.containsKey(hStoreFiles.get(3).getFileInfo().getActiveFileName()));
337  }
338
339  /*
340   * Verify that two cold blocks(both) are evicted when bucket reaches its capacity. The hot file
341   * remains in the cache.
342   */
343  @Test
344  public void testBlockEvictions() throws Exception {
345    initializeTestEnvironment();
346    long capacitySize = 40 * 1024;
347    int writeThreads = 3;
348    int writerQLen = 64;
349    int[] bucketSizes = new int[] { 8 * 1024 + 1024 };
350
351    // Setup: Create a bucket cache with lower capacity
352    BucketCache bucketCache =
353      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes,
354        writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf);
355
356    // Create three Cache keys with cold data files and a block with hot data.
357    // hStoreFiles.get(3) is a cold data file, while hStoreFiles.get(0) is a hot file.
358    Set<BlockCacheKey> cacheKeys = new HashSet<>();
359    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA));
360    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 8192, true, BlockType.DATA));
361    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA));
362
363    // Create dummy data to be cached and fill the cache completely.
364    CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3);
365
366    int blocksIter = 0;
367    for (BlockCacheKey key : cacheKeys) {
368      bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock());
369      // Ensure that the block is persisted to the file.
370      Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key)));
371    }
372
373    // Verify that the bucket cache contains 3 blocks.
374    assertEquals(3, bucketCache.getBackingMap().keySet().size());
375
376    // Add an additional block into cache with hot data which should trigger the eviction
377    BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA);
378    CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1);
379
380    bucketCache.cacheBlock(newKey, newBlock[0].getBlock());
381    Waiter.waitFor(defaultConf, 10000, 100,
382      () -> (bucketCache.getBackingMap().containsKey(newKey)));
383
384    // Verify that the bucket cache now contains 2 hot blocks blocks only.
385    // Both cold blocks of 8KB will be evicted to make room for 1 block of 8KB + an additional
386    // space.
387    validateBlocks(bucketCache.getBackingMap().keySet(), 2, 2, 0);
388  }
389
390  /*
391   * Verify that two cold blocks(both) are evicted when bucket reaches its capacity, but one cold
392   * block remains in the cache since the required space is freed.
393   */
394  @Test
395  public void testBlockEvictionsAllColdBlocks() throws Exception {
396    initializeTestEnvironment();
397    long capacitySize = 40 * 1024;
398    int writeThreads = 3;
399    int writerQLen = 64;
400    int[] bucketSizes = new int[] { 8 * 1024 + 1024 };
401
402    // Setup: Create a bucket cache with lower capacity
403    BucketCache bucketCache =
404      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes,
405        writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf);
406
407    // Create three Cache keys with three cold data blocks.
408    // hStoreFiles.get(3) is a cold data file.
409    Set<BlockCacheKey> cacheKeys = new HashSet<>();
410    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA));
411    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 8192, true, BlockType.DATA));
412    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 16384, true, BlockType.DATA));
413
414    // Create dummy data to be cached and fill the cache completely.
415    CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3);
416
417    int blocksIter = 0;
418    for (BlockCacheKey key : cacheKeys) {
419      bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock());
420      // Ensure that the block is persisted to the file.
421      Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key)));
422    }
423
424    // Verify that the bucket cache contains 3 blocks.
425    assertEquals(3, bucketCache.getBackingMap().keySet().size());
426
427    // Add an additional block into cache with hot data which should trigger the eviction
428    BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA);
429    CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1);
430
431    bucketCache.cacheBlock(newKey, newBlock[0].getBlock());
432    Waiter.waitFor(defaultConf, 10000, 100,
433      () -> (bucketCache.getBackingMap().containsKey(newKey)));
434
435    // Verify that the bucket cache now contains 1 cold block and a newly added hot block.
436    validateBlocks(bucketCache.getBackingMap().keySet(), 2, 1, 1);
437  }
438
439  /*
440   * Verify that a hot block evicted along with a cold block when bucket reaches its capacity.
441   */
442  @Test
443  public void testBlockEvictionsHotBlocks() throws Exception {
444    initializeTestEnvironment();
445    long capacitySize = 40 * 1024;
446    int writeThreads = 3;
447    int writerQLen = 64;
448    int[] bucketSizes = new int[] { 8 * 1024 + 1024 };
449
450    // Setup: Create a bucket cache with lower capacity
451    BucketCache bucketCache =
452      new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes,
453        writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf);
454
455    // Create three Cache keys with two hot data blocks and one cold data block
456    // hStoreFiles.get(0) is a hot data file and hStoreFiles.get(3) is a cold data file.
457    Set<BlockCacheKey> cacheKeys = new HashSet<>();
458    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA));
459    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 8192, true, BlockType.DATA));
460    cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA));
461
462    // Create dummy data to be cached and fill the cache completely.
463    CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3);
464
465    int blocksIter = 0;
466    for (BlockCacheKey key : cacheKeys) {
467      bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock());
468      // Ensure that the block is persisted to the file.
469      Waiter.waitFor(defaultConf, 10000, 100, () -> (bucketCache.getBackingMap().containsKey(key)));
470    }
471
472    // Verify that the bucket cache contains 3 blocks.
473    assertEquals(3, bucketCache.getBackingMap().keySet().size());
474
475    // Add an additional block which should evict the only cold block with an additional hot block.
476    BlockCacheKey newKey = new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA);
477    CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1);
478
479    bucketCache.cacheBlock(newKey, newBlock[0].getBlock());
480    Waiter.waitFor(defaultConf, 10000, 100,
481      () -> (bucketCache.getBackingMap().containsKey(newKey)));
482
483    // Verify that the bucket cache now contains 2 hot blocks.
484    // Only one of the older hot blocks is retained and other one is the newly added hot block.
485    validateBlocks(bucketCache.getBackingMap().keySet(), 2, 2, 0);
486  }
487
488  @Test
489  public void testFeatureKeyDisabled() throws Exception {
490    DataTieringManager.resetForTestingOnly();
491    defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, false);
492    initializeTestEnvironment();
493
494    try {
495      assertFalse(DataTieringManager.instantiate(defaultConf, testOnlineRegions));
496      // Verify that the DataaTieringManager instance is not instantiated in the
497      // instantiate call above.
498      assertNull(DataTieringManager.getInstance());
499
500      // Also validate that data temperature is not honoured.
501      long capacitySize = 40 * 1024;
502      int writeThreads = 3;
503      int writerQLen = 64;
504      int[] bucketSizes = new int[] { 8 * 1024 + 1024 };
505
506      // Setup: Create a bucket cache with lower capacity
507      BucketCache bucketCache =
508        new BucketCache("file:" + testDir + "/bucket.cache", capacitySize, 8192, bucketSizes,
509          writeThreads, writerQLen, null, DEFAULT_ERROR_TOLERATION_DURATION, defaultConf);
510
511      // Create three Cache keys with two hot data blocks and one cold data block
512      // hStoreFiles.get(0) is a hot data file and hStoreFiles.get(3) is a cold data file.
513      List<BlockCacheKey> cacheKeys = new ArrayList<>();
514      cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA));
515      cacheKeys.add(new BlockCacheKey(hStoreFiles.get(0).getPath(), 8192, true, BlockType.DATA));
516      cacheKeys.add(new BlockCacheKey(hStoreFiles.get(3).getPath(), 0, true, BlockType.DATA));
517
518      // Create dummy data to be cached and fill the cache completely.
519      CacheTestUtils.HFileBlockPair[] blocks = CacheTestUtils.generateHFileBlocks(8192, 3);
520
521      int blocksIter = 0;
522      for (BlockCacheKey key : cacheKeys) {
523        LOG.info("Adding {}", key);
524        bucketCache.cacheBlock(key, blocks[blocksIter++].getBlock());
525        // Ensure that the block is persisted to the file.
526        Waiter.waitFor(defaultConf, 10000, 100,
527          () -> (bucketCache.getBackingMap().containsKey(key)));
528      }
529
530      // Verify that the bucket cache contains 3 blocks.
531      assertEquals(3, bucketCache.getBackingMap().keySet().size());
532
533      // Add an additional hot block, which triggers eviction.
534      BlockCacheKey newKey =
535        new BlockCacheKey(hStoreFiles.get(2).getPath(), 0, true, BlockType.DATA);
536      CacheTestUtils.HFileBlockPair[] newBlock = CacheTestUtils.generateHFileBlocks(8192, 1);
537
538      bucketCache.cacheBlock(newKey, newBlock[0].getBlock());
539      Waiter.waitFor(defaultConf, 10000, 100,
540        () -> (bucketCache.getBackingMap().containsKey(newKey)));
541
542      // Verify that the bucket still contains the only cold block and one newly added hot block.
543      // The older hot blocks are evicted and data-tiering mechanism does not kick in to evict
544      // the cold block.
545      validateBlocks(bucketCache.getBackingMap().keySet(), 2, 1, 1);
546    } finally {
547      DataTieringManager.resetForTestingOnly();
548      defaultConf.setBoolean(DataTieringManager.GLOBAL_DATA_TIERING_ENABLED_KEY, true);
549      assertTrue(DataTieringManager.instantiate(defaultConf, testOnlineRegions));
550    }
551  }
552
553  @Test
554  public void testCacheConfigShouldCacheFile() throws Exception {
555    initializeTestEnvironment();
556    // Verify that the API shouldCacheFileBlock returns the result correctly.
557    // hStoreFiles[0], hStoreFiles[1], hStoreFiles[2] are hot files.
558    // hStoreFiles[3] is a cold file.
559    assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA,
560      hStoreFiles.get(0).getFileInfo().getHFileInfo(), hStoreFiles.get(0).getFileInfo().getConf()));
561    assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA,
562      hStoreFiles.get(1).getFileInfo().getHFileInfo(), hStoreFiles.get(1).getFileInfo().getConf()));
563    assertTrue(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA,
564      hStoreFiles.get(2).getFileInfo().getHFileInfo(), hStoreFiles.get(2).getFileInfo().getConf()));
565    assertFalse(cacheConf.shouldCacheBlockOnRead(BlockCategory.DATA,
566      hStoreFiles.get(3).getFileInfo().getHFileInfo(), hStoreFiles.get(3).getFileInfo().getConf()));
567  }
568
569  @Test
570  public void testCacheOnReadColdFile() throws Exception {
571    this.blockCache = initializeTestEnvironment();
572    // hStoreFiles[3] is a cold file. the blocks should not get loaded after a readBlock call.
573    HStoreFile hStoreFile = hStoreFiles.get(3);
574    BlockCacheKey cacheKey = new BlockCacheKey(hStoreFile.getPath(), 0, true, BlockType.DATA);
575    testCacheOnRead(hStoreFile, cacheKey, -1, false);
576  }
577
578  @Test
579  public void testCacheOnReadHotFile() throws Exception {
580    this.blockCache = initializeTestEnvironment();
581    // hStoreFiles[0] is a hot file. the blocks should get loaded after a readBlock call.
582    HStoreFile hStoreFile = hStoreFiles.get(0);
583    BlockCacheKey cacheKey =
584      new BlockCacheKey(hStoreFiles.get(0).getPath(), 0, true, BlockType.DATA);
585    testCacheOnRead(hStoreFile, cacheKey, -1, true);
586  }
587
588  private void testCacheOnRead(HStoreFile hStoreFile, BlockCacheKey key, long onDiskBlockSize,
589    boolean expectedCached) throws Exception {
590    // Execute the read block API which will try to cache the block if the block is a hot block.
591    hStoreFile.getReader().getHFileReader().readBlock(key.getOffset(), onDiskBlockSize, true, false,
592      false, false, key.getBlockType(), DataBlockEncoding.NONE);
593    // Validate that the hot block gets cached and cold block is not cached.
594    HFileBlock block = (HFileBlock) blockCache.getBlock(key, false, false, false);
595    if (expectedCached) {
596      assertNotNull(block);
597    } else {
598      assertNull(block);
599    }
600  }
601
602  private void validateBlocks(Set<BlockCacheKey> keys, int expectedTotalKeys, int expectedHotBlocks,
603    int expectedColdBlocks) {
604    int numHotBlocks = 0, numColdBlocks = 0;
605
606    Waiter.waitFor(defaultConf, 10000, 100, () -> (expectedTotalKeys == keys.size()));
607    int iter = 0;
608    for (BlockCacheKey key : keys) {
609      try {
610        if (dataTieringManager.isHotData(key)) {
611          numHotBlocks++;
612        } else {
613          numColdBlocks++;
614        }
615      } catch (Exception e) {
616        LOG.debug("Error validating priority for key {}", key, e);
617        fail(e.getMessage());
618      }
619    }
620    assertEquals(expectedHotBlocks, numHotBlocks);
621    assertEquals(expectedColdBlocks, numColdBlocks);
622  }
623
624  private void testDataTieringMethodWithPath(DataTieringMethodCallerWithPath caller, Path path,
625    boolean expectedResult, DataTieringException exception) {
626    try {
627      boolean value = caller.call(dataTieringManager, path);
628      if (exception != null) {
629        fail("Expected DataTieringException to be thrown");
630      }
631      assertEquals(expectedResult, value);
632    } catch (DataTieringException e) {
633      if (exception == null) {
634        fail("Unexpected DataTieringException: " + e.getMessage());
635      }
636      assertEquals(exception.getMessage(), e.getMessage());
637    }
638  }
639
640  private void testDataTieringMethodWithKey(DataTieringMethodCallerWithKey caller,
641    BlockCacheKey key, boolean expectedResult, DataTieringException exception) {
642    try {
643      boolean value = caller.call(dataTieringManager, key);
644      if (exception != null) {
645        fail("Expected DataTieringException to be thrown");
646      }
647      assertEquals(expectedResult, value);
648    } catch (DataTieringException e) {
649      if (exception == null) {
650        fail("Unexpected DataTieringException: " + e.getMessage());
651      }
652      assertEquals(exception.getMessage(), e.getMessage());
653    }
654  }
655
656  private void testDataTieringMethodWithPathExpectingException(
657    DataTieringMethodCallerWithPath caller, Path path, DataTieringException exception) {
658    testDataTieringMethodWithPath(caller, path, false, exception);
659  }
660
661  private void testDataTieringMethodWithPathNoException(DataTieringMethodCallerWithPath caller,
662    Path path, boolean expectedResult) {
663    testDataTieringMethodWithPath(caller, path, expectedResult, null);
664  }
665
666  private void testDataTieringMethodWithKeyExpectingException(DataTieringMethodCallerWithKey caller,
667    BlockCacheKey key, DataTieringException exception) {
668    testDataTieringMethodWithKey(caller, key, false, exception);
669  }
670
671  private void testDataTieringMethodWithKeyNoException(DataTieringMethodCallerWithKey caller,
672    BlockCacheKey key, boolean expectedResult) {
673    testDataTieringMethodWithKey(caller, key, expectedResult, null);
674  }
675
676  private static BlockCache initializeTestEnvironment() throws IOException {
677    BlockCache blockCache = setupFileSystemAndCache();
678    setupOnlineRegions(blockCache);
679    return blockCache;
680  }
681
682  private static BlockCache setupFileSystemAndCache() throws IOException {
683    fs = HFileSystem.get(defaultConf);
684    BlockCache blockCache = BlockCacheFactory.createBlockCache(defaultConf);
685    cacheConf = new CacheConfig(defaultConf, blockCache);
686    return blockCache;
687  }
688
689  private static void setupOnlineRegions(BlockCache blockCache) throws IOException {
690    testOnlineRegions.clear();
691    hStoreFiles.clear();
692    long day = 24 * 60 * 60 * 1000;
693    long currentTime = System.currentTimeMillis();
694
695    HRegion region1 = createHRegion("table1", blockCache);
696
697    HStore hStore11 = createHStore(region1, "cf1", getConfWithCustomCellDataTieringEnabled(day));
698    hStoreFiles.add(createHStoreFile(hStore11.getStoreContext().getFamilyStoreDirectoryPath(),
699      hStore11.getReadOnlyConfiguration(), currentTime, region1.getRegionFileSystem()));
700    hStore11.refreshStoreFiles();
701    HStore hStore12 = createHStore(region1, "cf2");
702    hStoreFiles.add(createHStoreFile(hStore12.getStoreContext().getFamilyStoreDirectoryPath(),
703      hStore12.getReadOnlyConfiguration(), currentTime - day, region1.getRegionFileSystem()));
704    hStore12.refreshStoreFiles();
705
706    region1.stores.put(Bytes.toBytes("cf1"), hStore11);
707    region1.stores.put(Bytes.toBytes("cf2"), hStore12);
708
709    HRegion region2 = createHRegion("table2",
710      getConfWithCustomCellDataTieringEnabled((long) (2.5 * day)), blockCache);
711
712    HStore hStore21 = createHStore(region2, "cf1");
713    hStoreFiles.add(createHStoreFile(hStore21.getStoreContext().getFamilyStoreDirectoryPath(),
714      hStore21.getReadOnlyConfiguration(), currentTime - 2 * day, region2.getRegionFileSystem()));
715    hStore21.refreshStoreFiles();
716    HStore hStore22 = createHStore(region2, "cf2");
717    hStoreFiles.add(createHStoreFile(hStore22.getStoreContext().getFamilyStoreDirectoryPath(),
718      hStore22.getReadOnlyConfiguration(), currentTime - 3 * day, region2.getRegionFileSystem()));
719    hStore22.refreshStoreFiles();
720
721    region2.stores.put(Bytes.toBytes("cf1"), hStore21);
722    region2.stores.put(Bytes.toBytes("cf2"), hStore22);
723
724    for (HStoreFile file : hStoreFiles) {
725      file.initReader();
726    }
727
728    testOnlineRegions.put(region1.getRegionInfo().getEncodedName(), region1);
729    testOnlineRegions.put(region2.getRegionInfo().getEncodedName(), region2);
730  }
731
732  private static HRegion createHRegion(String table, BlockCache blockCache) throws IOException {
733    return createHRegion(table, defaultConf, blockCache);
734  }
735
736  private static HRegion createHRegion(String table, Configuration conf, BlockCache blockCache)
737    throws IOException {
738    TableName tableName = TableName.valueOf(table);
739
740    TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName)
741      .setValue(DataTieringManager.DATATIERING_KEY, conf.get(DataTieringManager.DATATIERING_KEY))
742      .setValue(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY,
743        conf.get(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY))
744      .build();
745    RegionInfo hri = RegionInfoBuilder.newBuilder(tableName).build();
746
747    Configuration testConf = new Configuration(conf);
748    CommonFSUtils.setRootDir(testConf, testDir);
749    HRegionFileSystem regionFs = HRegionFileSystem.createRegionOnFileSystem(testConf, fs,
750      CommonFSUtils.getTableDir(testDir, hri.getTable()), hri);
751
752    HRegion region = new HRegion(regionFs, null, conf, htd, null);
753    // Manually sets the BlockCache for the HRegion instance.
754    // This is necessary because the region server is not started within this method,
755    // and therefore the BlockCache needs to be explicitly configured.
756    region.setBlockCache(blockCache);
757    return region;
758  }
759
760  private static HStore createHStore(HRegion region, String columnFamily) throws IOException {
761    return createHStore(region, columnFamily, defaultConf);
762  }
763
764  private static HStore createHStore(HRegion region, String columnFamily, Configuration conf)
765    throws IOException {
766    ColumnFamilyDescriptor columnFamilyDescriptor =
767      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily))
768        .setValue(DataTieringManager.DATATIERING_KEY, conf.get(DataTieringManager.DATATIERING_KEY))
769        .setValue(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY,
770          conf.get(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY))
771        .build();
772
773    return new HStore(region, columnFamilyDescriptor, conf, false);
774  }
775
776  private static HStoreFile createHStoreFile(Path storeDir, Configuration conf, long timestamp,
777    HRegionFileSystem regionFs) throws IOException {
778    String columnFamily = storeDir.getName();
779
780    StoreFileWriter storeFileWriter = new StoreFileWriter.Builder(conf, cacheConf, fs)
781      .withOutputDir(storeDir).withFileContext(new HFileContextBuilder().build()).build();
782
783    writeStoreFileRandomData(storeFileWriter, Bytes.toBytes(columnFamily), timestamp);
784
785    StoreContext storeContext = StoreContext.getBuilder().withRegionFileSystem(regionFs).build();
786    StoreFileTracker sft = StoreFileTrackerFactory.create(conf, true, storeContext);
787    return new HStoreFile(fs, storeFileWriter.getPath(), conf, cacheConf, BloomType.NONE, true,
788      sft);
789  }
790
791  private static Configuration getConfWithCustomCellDataTieringEnabled(long hotDataAge) {
792    Configuration conf = new Configuration(defaultConf);
793    conf.set(DataTieringManager.DATATIERING_KEY, DataTieringType.CUSTOM.name());
794    conf.set(DataTieringManager.DATATIERING_HOT_DATA_AGE_KEY, String.valueOf(hotDataAge));
795    return conf;
796  }
797
798  /**
799   * Writes random data to a store file with rows arranged in lexicographically increasing order.
800   * Each row is generated using the {@link #nextString()} method, ensuring that each subsequent row
801   * is lexicographically larger than the previous one.
802   */
803  private static void writeStoreFileRandomData(final StoreFileWriter writer, byte[] columnFamily,
804    long timestamp) throws IOException {
805    int cellsPerFile = 10;
806    byte[] qualifier = Bytes.toBytes("qualifier");
807    byte[] value = generateRandomBytes(4 * 1024);
808    try {
809      for (int i = 0; i < cellsPerFile; i++) {
810        byte[] row = Bytes.toBytes(nextString());
811        writer.append(new KeyValue(row, columnFamily, qualifier, timestamp, value));
812      }
813    } finally {
814      writer.appendTrackedTimestampsToMetadata();
815      TimeRangeTracker timeRangeTracker = TimeRangeTracker.create(TimeRangeTracker.Type.NON_SYNC);
816      timeRangeTracker.setMin(timestamp);
817      timeRangeTracker.setMax(timestamp);
818      writer.appendCustomCellTimestampsToMetadata(timeRangeTracker);
819      writer.close();
820    }
821  }
822
823  private static byte[] generateRandomBytes(int sizeInBytes) {
824    Random random = new Random();
825    byte[] randomBytes = new byte[sizeInBytes];
826    random.nextBytes(randomBytes);
827    return randomBytes;
828  }
829
830  /**
831   * Returns the lexicographically larger string every time it's called.
832   */
833  private static String nextString() {
834    if (rowKeyString == null || rowKeyString.isEmpty()) {
835      rowKeyString = "a";
836    }
837    char lastChar = rowKeyString.charAt(rowKeyString.length() - 1);
838    if (lastChar < 'z') {
839      rowKeyString = rowKeyString.substring(0, rowKeyString.length() - 1) + (char) (lastChar + 1);
840    } else {
841      rowKeyString = rowKeyString + "a";
842    }
843    return rowKeyString;
844  }
845}