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