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