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