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