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.io.hfile;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.ByteArrayOutputStream;
025import java.io.DataOutput;
026import java.io.DataOutputStream;
027import java.io.IOException;
028import java.nio.ByteBuffer;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Random;
035import java.util.Set;
036import java.util.concurrent.atomic.AtomicInteger;
037import org.apache.hadoop.conf.Configuration;
038import org.apache.hadoop.fs.FSDataInputStream;
039import org.apache.hadoop.fs.FSDataOutputStream;
040import org.apache.hadoop.fs.FileSystem;
041import org.apache.hadoop.fs.Path;
042import org.apache.hadoop.hbase.CellBuilderType;
043import org.apache.hadoop.hbase.CellComparatorImpl;
044import org.apache.hadoop.hbase.CellUtil;
045import org.apache.hadoop.hbase.ExtendedCellBuilderFactory;
046import org.apache.hadoop.hbase.HBaseClassTestRule;
047import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
048import org.apache.hadoop.hbase.HBaseTestingUtil;
049import org.apache.hadoop.hbase.HConstants;
050import org.apache.hadoop.hbase.KeyValue;
051import org.apache.hadoop.hbase.KeyValueUtil;
052import org.apache.hadoop.hbase.PrivateCellUtil;
053import org.apache.hadoop.hbase.fs.HFileSystem;
054import org.apache.hadoop.hbase.io.ByteBuffAllocator;
055import org.apache.hadoop.hbase.io.FSDataInputStreamWrapper;
056import org.apache.hadoop.hbase.io.compress.Compression;
057import org.apache.hadoop.hbase.io.compress.Compression.Algorithm;
058import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
059import org.apache.hadoop.hbase.io.encoding.IndexBlockEncoding;
060import org.apache.hadoop.hbase.io.hfile.HFile.Writer;
061import org.apache.hadoop.hbase.io.hfile.HFileBlockIndex.BlockIndexReader;
062import org.apache.hadoop.hbase.io.hfile.NoOpIndexBlockEncoder.NoOpEncodedSeeker;
063import org.apache.hadoop.hbase.nio.ByteBuff;
064import org.apache.hadoop.hbase.nio.MultiByteBuff;
065import org.apache.hadoop.hbase.nio.RefCnt;
066import org.apache.hadoop.hbase.testclassification.IOTests;
067import org.apache.hadoop.hbase.testclassification.MediumTests;
068import org.apache.hadoop.hbase.util.Bytes;
069import org.apache.hadoop.hbase.util.ClassSize;
070import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
071import org.junit.Before;
072import org.junit.ClassRule;
073import org.junit.Test;
074import org.junit.experimental.categories.Category;
075import org.junit.runner.RunWith;
076import org.junit.runners.Parameterized;
077import org.junit.runners.Parameterized.Parameters;
078import org.slf4j.Logger;
079import org.slf4j.LoggerFactory;
080
081import org.apache.hbase.thirdparty.io.netty.util.ResourceLeakDetector;
082
083@RunWith(Parameterized.class)
084@Category({ IOTests.class, MediumTests.class })
085public class TestHFileBlockIndex {
086
087  @ClassRule
088  public static final HBaseClassTestRule CLASS_RULE =
089    HBaseClassTestRule.forClass(TestHFileBlockIndex.class);
090
091  @Parameters
092  public static Collection<Object[]> compressionAlgorithms() {
093    return HBaseCommonTestingUtil.COMPRESSION_ALGORITHMS_PARAMETERIZED;
094  }
095
096  public TestHFileBlockIndex(Compression.Algorithm compr) {
097    this.compr = compr;
098  }
099
100  private static final Logger LOG = LoggerFactory.getLogger(TestHFileBlockIndex.class);
101  private static final Random RNG = new Random(); // This test depends on Random#setSeed
102  private static final int NUM_DATA_BLOCKS = 1000;
103  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
104
105  private static final int SMALL_BLOCK_SIZE = 4096;
106  private static final int NUM_KV = 10000;
107
108  private static FileSystem fs;
109  private Path path;
110  private long rootIndexOffset;
111  private int numRootEntries;
112  private int numLevels;
113  private static final List<byte[]> keys = new ArrayList<>();
114  private final Compression.Algorithm compr;
115  private byte[] firstKeyInFile;
116  private Configuration conf;
117
118  private static final int[] INDEX_CHUNK_SIZES = { 4096, 512, 384 };
119  private static final int[] EXPECTED_NUM_LEVELS = { 2, 3, 4 };
120  private static final int[] UNCOMPRESSED_INDEX_SIZES = { 19187, 21813, 23086 };
121
122  private static final boolean includesMemstoreTS = true;
123
124  static {
125    assert INDEX_CHUNK_SIZES.length == EXPECTED_NUM_LEVELS.length;
126    assert INDEX_CHUNK_SIZES.length == UNCOMPRESSED_INDEX_SIZES.length;
127  }
128
129  @Before
130  public void setUp() throws IOException {
131    keys.clear();
132    firstKeyInFile = null;
133    conf = TEST_UTIL.getConfiguration();
134    RNG.setSeed(2389757);
135
136    // This test requires at least HFile format version 2.
137    conf.setInt(HFile.FORMAT_VERSION_KEY, HFile.MAX_FORMAT_VERSION);
138
139    fs = HFileSystem.get(conf);
140  }
141
142  @Test
143  public void testBlockIndex() throws IOException {
144    testBlockIndexInternals(false);
145    clear();
146    testBlockIndexInternals(true);
147  }
148
149  private void writeDataBlocksAndCreateIndex(HFileBlock.Writer hbw, FSDataOutputStream outputStream,
150    HFileBlockIndex.BlockIndexWriter biw) throws IOException {
151    for (int i = 0; i < NUM_DATA_BLOCKS; ++i) {
152      hbw.startWriting(BlockType.DATA).write(Bytes.toBytes(String.valueOf(RNG.nextInt(1000))));
153      long blockOffset = outputStream.getPos();
154      hbw.writeHeaderAndData(outputStream);
155
156      byte[] firstKey = null;
157      byte[] family = Bytes.toBytes("f");
158      byte[] qualifier = Bytes.toBytes("q");
159      for (int j = 0; j < 16; ++j) {
160        byte[] k = new KeyValue(RandomKeyValueUtil.randomOrderedKey(RNG, i * 16 + j), family,
161          qualifier, EnvironmentEdgeManager.currentTime(), KeyValue.Type.Put).getKey();
162        keys.add(k);
163        if (j == 8) {
164          firstKey = k;
165        }
166      }
167      assertTrue(firstKey != null);
168      if (firstKeyInFile == null) {
169        firstKeyInFile = firstKey;
170      }
171      biw.addEntry(firstKey, blockOffset, hbw.getOnDiskSizeWithHeader());
172
173      writeInlineBlocks(hbw, outputStream, biw, false);
174    }
175    writeInlineBlocks(hbw, outputStream, biw, true);
176    rootIndexOffset = biw.writeIndexBlocks(outputStream);
177    outputStream.close();
178  }
179
180  @Test
181  public void testBlockIndexWithOffHeapBuffer() throws Exception {
182    ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
183    path = new Path(TEST_UTIL.getDataTestDir(), "block_index_testBlockIndexWithOffHeapBuffer");
184    assertEquals(0, keys.size());
185    HFileContext meta = new HFileContextBuilder().withHBaseCheckSum(true)
186      .withIncludesMvcc(includesMemstoreTS).withIncludesTags(true).withCompression(compr)
187      .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM).build();
188    ByteBuffAllocator allocator = ByteBuffAllocator.create(TEST_UTIL.getConfiguration(), true);
189    HFileBlock.Writer hbw = new HFileBlock.Writer(TEST_UTIL.getConfiguration(), null, meta,
190      allocator, meta.getBlocksize());
191    FSDataOutputStream outputStream = fs.create(path);
192
193    final AtomicInteger counter = new AtomicInteger();
194    RefCnt.detector.setLeakListener(new ResourceLeakDetector.LeakListener() {
195      @Override
196      public void onLeak(String s, String s1) {
197        counter.incrementAndGet();
198      }
199    });
200
201    long maxSize = NUM_DATA_BLOCKS * 1000;
202    long blockSize = 1000;
203    LruBlockCache cache = new LruBlockCache(maxSize, blockSize);
204    CacheConfig cacheConfig = new CacheConfig(TEST_UTIL.getConfiguration(), null, cache, allocator);
205
206    HFileBlockIndex.BlockIndexWriter biw =
207      new HFileBlockIndex.BlockIndexWriter(hbw, cacheConfig, path.getName(), null);
208
209    writeDataBlocksAndCreateIndex(hbw, outputStream, biw);
210    for (int i = 0; i < 30 && counter.get() == 0; i++) {
211      System.gc();
212      try {
213        Thread.sleep(1000);
214      } catch (InterruptedException e) {
215        Thread.currentThread().interrupt();
216        break;
217      }
218      allocator.allocate(128 * 1024).release();
219    }
220    assertEquals(0, counter.get());
221  }
222
223  @Test
224  public void testIntermediateIndexCacheOnWriteDoesNotLeak() throws Exception {
225    Configuration localConf = new Configuration(TEST_UTIL.getConfiguration());
226    localConf.setInt(HFile.FORMAT_VERSION_KEY, HFile.MAX_FORMAT_VERSION);
227    localConf.setBoolean(CacheConfig.CACHE_INDEX_BLOCKS_ON_WRITE_KEY, true);
228    localConf.setInt(ByteBuffAllocator.BUFFER_SIZE_KEY, 4096);
229    localConf.setInt(ByteBuffAllocator.MAX_BUFFER_COUNT_KEY, 32);
230    localConf.setInt(ByteBuffAllocator.MIN_ALLOCATE_SIZE_KEY, 0);
231    ByteBuffAllocator allocator = ByteBuffAllocator.create(localConf, true);
232    List<ByteBuff> buffers = new ArrayList<>();
233    for (int i = 0; i < allocator.getTotalBufferCount(); i++) {
234      buffers.add(allocator.allocateOneBuffer());
235      assertEquals(0, allocator.getFreeBufferCount());
236    }
237    buffers.forEach(ByteBuff::release);
238    assertEquals(allocator.getTotalBufferCount(), allocator.getFreeBufferCount());
239    ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
240    final AtomicInteger counter = new AtomicInteger();
241    RefCnt.detector.setLeakListener(new ResourceLeakDetector.LeakListener() {
242      @Override
243      public void onLeak(String s, String s1) {
244        counter.incrementAndGet();
245      }
246    });
247
248    Path localPath = new Path(TEST_UTIL.getDataTestDir(),
249      "block_index_testIntermediateIndexCacheOnWriteDoesNotLeak_" + compr);
250    HFileContext meta = new HFileContextBuilder().withHBaseCheckSum(true)
251      .withIncludesMvcc(includesMemstoreTS).withIncludesTags(true).withCompression(compr)
252      .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM).build();
253    HFileBlock.Writer hbw =
254      new HFileBlock.Writer(localConf, null, meta, allocator, meta.getBlocksize());
255    FSDataOutputStream outputStream = fs.create(localPath);
256    LruBlockCache cache = new LruBlockCache(8 * 1024 * 1024, 1024, true, localConf);
257    CacheConfig cacheConfig = new CacheConfig(localConf, null, cache, allocator);
258    HFileBlockIndex.BlockIndexWriter biw =
259      new HFileBlockIndex.BlockIndexWriter(hbw, cacheConfig, localPath.getName(), null);
260    biw.setMaxChunkSize(512);
261
262    try {
263      writeDataBlocksAndCreateIndex(hbw, outputStream, biw);
264      assertTrue(biw.getNumLevels() >= 3);
265      for (int i = 0; i < 30 && counter.get() == 0; i++) {
266        System.gc();
267        try {
268          Thread.sleep(1000);
269        } catch (InterruptedException e) {
270          Thread.currentThread().interrupt();
271          break;
272        }
273        allocator.allocate(128 * 1024).release();
274      }
275      assertEquals(0, counter.get());
276    } finally {
277      hbw.release();
278      cache.shutdown();
279      allocator.clean();
280      fs.delete(localPath, false);
281    }
282  }
283
284  private void clear() throws IOException {
285    keys.clear();
286    firstKeyInFile = null;
287    conf = TEST_UTIL.getConfiguration();
288    RNG.setSeed(2389757);
289
290    // This test requires at least HFile format version 2.
291    conf.setInt(HFile.FORMAT_VERSION_KEY, 3);
292
293    fs = HFileSystem.get(conf);
294  }
295
296  private void testBlockIndexInternals(boolean useTags) throws IOException {
297    path = new Path(TEST_UTIL.getDataTestDir(), "block_index_" + compr + useTags);
298    writeWholeIndex(useTags);
299    readIndex(useTags);
300  }
301
302  /**
303   * A wrapper around a block reader which only caches the results of the last operation. Not
304   * thread-safe.
305   */
306  private static class BlockReaderWrapper implements HFile.CachingBlockReader {
307
308    private HFileBlock.FSReader realReader;
309    private long prevOffset;
310    private long prevOnDiskSize;
311    private boolean prevPread;
312    private HFileBlock prevBlock;
313
314    public int hitCount = 0;
315    public int missCount = 0;
316
317    public BlockReaderWrapper(HFileBlock.FSReader realReader) {
318      this.realReader = realReader;
319    }
320
321    @Override
322    public HFileBlock readBlock(long offset, long onDiskSize, boolean cacheBlock, boolean pread,
323      boolean isCompaction, boolean updateCacheMetrics, BlockType expectedBlockType,
324      DataBlockEncoding expectedDataBlockEncoding) throws IOException {
325      return readBlock(offset, onDiskSize, cacheBlock, pread, isCompaction, updateCacheMetrics,
326        expectedBlockType, expectedDataBlockEncoding, false);
327    }
328
329    @Override
330    public HFileBlock readBlock(long offset, long onDiskSize, boolean cacheBlock, boolean pread,
331      boolean isCompaction, boolean updateCacheMetrics, BlockType expectedBlockType,
332      DataBlockEncoding expectedDataBlockEncoding, boolean cacheOnly) throws IOException {
333      if (offset == prevOffset && onDiskSize == prevOnDiskSize && pread == prevPread) {
334        hitCount += 1;
335        return prevBlock;
336      }
337
338      missCount += 1;
339      prevBlock = realReader.readBlockData(offset, onDiskSize, pread, false, true);
340      prevOffset = offset;
341      prevOnDiskSize = onDiskSize;
342      prevPread = pread;
343
344      return prevBlock;
345    }
346  }
347
348  private void readIndex(boolean useTags) throws IOException {
349    long fileSize = fs.getFileStatus(path).getLen();
350    LOG.info("Size of {}: {} compression={}", path, fileSize, compr.toString());
351
352    FSDataInputStream istream = fs.open(path);
353    HFileContext meta =
354      new HFileContextBuilder().withHBaseCheckSum(true).withIncludesMvcc(includesMemstoreTS)
355        .withIncludesTags(useTags).withCompression(compr).build();
356    ReaderContext context = new ReaderContextBuilder().withFileSystemAndPath(fs, path).build();
357    HFileBlock.FSReader blockReader =
358      new HFileBlock.FSReaderImpl(context, meta, ByteBuffAllocator.HEAP, conf);
359
360    BlockReaderWrapper brw = new BlockReaderWrapper(blockReader);
361    HFileBlockIndex.BlockIndexReader indexReader =
362      new HFileBlockIndex.CellBasedKeyBlockIndexReader(CellComparatorImpl.COMPARATOR, numLevels);
363
364    indexReader.readRootIndex(blockReader.blockRange(rootIndexOffset, fileSize)
365      .nextBlockWithBlockType(BlockType.ROOT_INDEX), numRootEntries);
366
367    long prevOffset = -1;
368    int i = 0;
369    int expectedHitCount = 0;
370    int expectedMissCount = 0;
371    LOG.info("Total number of keys: " + keys.size());
372    for (byte[] key : keys) {
373      assertTrue(key != null);
374      assertTrue(indexReader != null);
375      KeyValue.KeyOnlyKeyValue keyOnlyKey = new KeyValue.KeyOnlyKeyValue(key, 0, key.length);
376      HFileBlock b = indexReader.seekToDataBlock(keyOnlyKey, null, true, true, false, null, brw);
377      if (
378        PrivateCellUtil.compare(CellComparatorImpl.COMPARATOR, keyOnlyKey, firstKeyInFile, 0,
379          firstKeyInFile.length) < 0
380      ) {
381        assertTrue(b == null);
382        ++i;
383        continue;
384      }
385
386      String keyStr = "key #" + i + ", " + Bytes.toStringBinary(key);
387
388      assertTrue("seekToDataBlock failed for " + keyStr, b != null);
389
390      if (prevOffset == b.getOffset()) {
391        assertEquals(++expectedHitCount, brw.hitCount);
392      } else {
393        LOG.info("First key in a new block: " + keyStr + ", block offset: " + b.getOffset() + ")");
394        assertTrue(b.getOffset() > prevOffset);
395        assertEquals(++expectedMissCount, brw.missCount);
396        prevOffset = b.getOffset();
397      }
398      ++i;
399    }
400
401    istream.close();
402  }
403
404  private void writeWholeIndex(boolean useTags) throws IOException {
405    assertEquals(0, keys.size());
406    HFileContext meta = new HFileContextBuilder().withHBaseCheckSum(true)
407      .withIncludesMvcc(includesMemstoreTS).withIncludesTags(useTags).withCompression(compr)
408      .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM).build();
409    HFileBlock.Writer hbw = new HFileBlock.Writer(TEST_UTIL.getConfiguration(), null, meta);
410    FSDataOutputStream outputStream = fs.create(path);
411    HFileBlockIndex.BlockIndexWriter biw =
412      new HFileBlockIndex.BlockIndexWriter(hbw, null, null, null);
413    writeDataBlocksAndCreateIndex(hbw, outputStream, biw);
414
415    numLevels = biw.getNumLevels();
416    numRootEntries = biw.getNumRootEntries();
417
418    LOG.info("Index written: numLevels=" + numLevels + ", numRootEntries=" + numRootEntries
419      + ", rootIndexOffset=" + rootIndexOffset);
420  }
421
422  private void writeInlineBlocks(HFileBlock.Writer hbw, FSDataOutputStream outputStream,
423    HFileBlockIndex.BlockIndexWriter biw, boolean isClosing) throws IOException {
424    while (biw.shouldWriteBlock(isClosing)) {
425      long offset = outputStream.getPos();
426      biw.writeInlineBlock(hbw.startWriting(biw.getInlineBlockType()));
427      hbw.writeHeaderAndData(outputStream);
428      biw.blockWritten(offset, hbw.getOnDiskSizeWithHeader(),
429        hbw.getUncompressedSizeWithoutHeader());
430      LOG.info(
431        "Wrote an inline index block at " + offset + ", size " + hbw.getOnDiskSizeWithHeader());
432    }
433  }
434
435  private static final long getDummyFileOffset(int i) {
436    return i * 185 + 379;
437  }
438
439  private static final int getDummyOnDiskSize(int i) {
440    return i * i * 37 + i * 19 + 13;
441  }
442
443  @Test
444  public void testSecondaryIndexBinarySearch() throws IOException {
445    int numTotalKeys = 99;
446    assertTrue(numTotalKeys % 2 == 1); // Ensure no one made this even.
447
448    // We only add odd-index keys into the array that we will binary-search.
449    int numSearchedKeys = (numTotalKeys - 1) / 2;
450
451    ByteArrayOutputStream baos = new ByteArrayOutputStream();
452    DataOutputStream dos = new DataOutputStream(baos);
453
454    dos.writeInt(numSearchedKeys);
455    int curAllEntriesSize = 0;
456    int numEntriesAdded = 0;
457
458    // Only odd-index elements of this array are used to keep the secondary
459    // index entries of the corresponding keys.
460    int secondaryIndexEntries[] = new int[numTotalKeys];
461
462    for (int i = 0; i < numTotalKeys; ++i) {
463      byte[] k = RandomKeyValueUtil.randomOrderedKey(RNG, i * 2);
464      KeyValue cell = new KeyValue(k, Bytes.toBytes("f"), Bytes.toBytes("q"), Bytes.toBytes("val"));
465      // KeyValue cell = new KeyValue.KeyOnlyKeyValue(k, 0, k.length);
466      keys.add(cell.getKey());
467      String msgPrefix = "Key #" + i + " (" + Bytes.toStringBinary(k) + "): ";
468      StringBuilder padding = new StringBuilder();
469      while (msgPrefix.length() + padding.length() < 70)
470        padding.append(' ');
471      msgPrefix += padding;
472      if (i % 2 == 1) {
473        dos.writeInt(curAllEntriesSize);
474        secondaryIndexEntries[i] = curAllEntriesSize;
475        LOG.info(
476          msgPrefix + "secondary index entry #" + ((i - 1) / 2) + ", offset " + curAllEntriesSize);
477        curAllEntriesSize += cell.getKey().length + HFileBlockIndex.SECONDARY_INDEX_ENTRY_OVERHEAD;
478        ++numEntriesAdded;
479      } else {
480        secondaryIndexEntries[i] = -1;
481        LOG.info(msgPrefix + "not in the searched array");
482      }
483    }
484
485    // Make sure the keys are increasing.
486    for (int i = 0; i < keys.size() - 1; ++i)
487      assertTrue(CellComparatorImpl.COMPARATOR.compare(
488        new KeyValue.KeyOnlyKeyValue(keys.get(i), 0, keys.get(i).length),
489        new KeyValue.KeyOnlyKeyValue(keys.get(i + 1), 0, keys.get(i + 1).length)) < 0);
490
491    dos.writeInt(curAllEntriesSize);
492    assertEquals(numSearchedKeys, numEntriesAdded);
493    int secondaryIndexOffset = dos.size();
494    assertEquals(Bytes.SIZEOF_INT * (numSearchedKeys + 2), secondaryIndexOffset);
495
496    for (int i = 1; i <= numTotalKeys - 1; i += 2) {
497      assertEquals(dos.size(), secondaryIndexOffset + secondaryIndexEntries[i]);
498      long dummyFileOffset = getDummyFileOffset(i);
499      int dummyOnDiskSize = getDummyOnDiskSize(i);
500      LOG.debug("Storing file offset=" + dummyFileOffset + " and onDiskSize=" + dummyOnDiskSize
501        + " at offset " + dos.size());
502      dos.writeLong(dummyFileOffset);
503      dos.writeInt(dummyOnDiskSize);
504      LOG.debug("Stored key " + ((i - 1) / 2) + " at offset " + dos.size());
505      dos.write(keys.get(i));
506    }
507
508    dos.writeInt(curAllEntriesSize);
509
510    ByteBuffer nonRootIndex = ByteBuffer.wrap(baos.toByteArray());
511    for (int i = 0; i < numTotalKeys; ++i) {
512      byte[] searchKey = keys.get(i);
513      byte[] arrayHoldingKey = new byte[searchKey.length + searchKey.length / 2];
514
515      // To make things a bit more interesting, store the key we are looking
516      // for at a non-zero offset in a new array.
517      System.arraycopy(searchKey, 0, arrayHoldingKey, searchKey.length / 2, searchKey.length);
518
519      KeyValue.KeyOnlyKeyValue cell =
520        new KeyValue.KeyOnlyKeyValue(arrayHoldingKey, searchKey.length / 2, searchKey.length);
521      int searchResult = BlockIndexReader.binarySearchNonRootIndex(cell,
522        new MultiByteBuff(nonRootIndex), CellComparatorImpl.COMPARATOR);
523      String lookupFailureMsg =
524        "Failed to look up key #" + i + " (" + Bytes.toStringBinary(searchKey) + ")";
525
526      int expectedResult;
527      int referenceItem;
528
529      if (i % 2 == 1) {
530        // This key is in the array we search as the element (i - 1) / 2. Make
531        // sure we find it.
532        expectedResult = (i - 1) / 2;
533        referenceItem = i;
534      } else {
535        // This key is not in the array but between two elements on the array,
536        // in the beginning, or in the end. The result should be the previous
537        // key in the searched array, or -1 for i = 0.
538        expectedResult = i / 2 - 1;
539        referenceItem = i - 1;
540      }
541
542      assertEquals(lookupFailureMsg, expectedResult, searchResult);
543
544      // Now test we can get the offset and the on-disk-size using a
545      // higher-level API function.s
546      boolean locateBlockResult =
547        (BlockIndexReader.locateNonRootIndexEntry(new MultiByteBuff(nonRootIndex), cell,
548          CellComparatorImpl.COMPARATOR) != -1);
549
550      if (i == 0) {
551        assertFalse(locateBlockResult);
552      } else {
553        assertTrue(locateBlockResult);
554        String errorMsg = "i=" + i + ", position=" + nonRootIndex.position();
555        assertEquals(errorMsg, getDummyFileOffset(referenceItem), nonRootIndex.getLong());
556        assertEquals(errorMsg, getDummyOnDiskSize(referenceItem), nonRootIndex.getInt());
557      }
558    }
559
560  }
561
562  @Test
563  public void testBlockIndexChunk() throws IOException {
564    BlockIndexChunk c = new HFileBlockIndex.BlockIndexChunkImpl();
565    HFileIndexBlockEncoder indexBlockEncoder = NoOpIndexBlockEncoder.INSTANCE;
566    ByteArrayOutputStream baos = new ByteArrayOutputStream();
567    int N = 1000;
568    int[] numSubEntriesAt = new int[N];
569    int numSubEntries = 0;
570    for (int i = 0; i < N; ++i) {
571      baos.reset();
572      DataOutputStream dos = new DataOutputStream(baos);
573      indexBlockEncoder.encode(c, false, dos);
574      assertEquals(c.getNonRootSize(), dos.size());
575
576      baos.reset();
577      dos = new DataOutputStream(baos);
578      indexBlockEncoder.encode(c, true, dos);
579      assertEquals(c.getRootSize(), dos.size());
580
581      byte[] k = RandomKeyValueUtil.randomOrderedKey(RNG, i);
582      numSubEntries += RNG.nextInt(5) + 1;
583      keys.add(k);
584      c.add(k, getDummyFileOffset(i), getDummyOnDiskSize(i), numSubEntries);
585    }
586
587    // Test the ability to look up the entry that contains a particular
588    // deeper-level index block's entry ("sub-entry"), assuming a global
589    // 0-based ordering of sub-entries. This is needed for mid-key calculation.
590    for (int i = 0; i < N; ++i) {
591      for (int j = i == 0 ? 0 : numSubEntriesAt[i - 1]; j < numSubEntriesAt[i]; ++j) {
592        assertEquals(i, c.getEntryBySubEntry(j));
593      }
594    }
595  }
596
597  /** Checks if the HeapSize calculator is within reason */
598  @Test
599  public void testHeapSizeForBlockIndex() throws IOException {
600    Class<HFileBlockIndex.BlockIndexReader> cl = HFileBlockIndex.BlockIndexReader.class;
601    long expected = ClassSize.estimateBase(cl, false);
602
603    HFileBlockIndex.BlockIndexReader bi = new HFileBlockIndex.ByteArrayKeyBlockIndexReader(1);
604    long actual = bi.heapSize();
605
606    // Since the arrays in BlockIndex(byte [][] blockKeys, long [] blockOffsets,
607    // int [] blockDataSizes) are all null they are not going to show up in the
608    // HeapSize calculation, so need to remove those array costs from expected.
609    // Already the block keys are not there in this case
610    expected -= ClassSize.align(2 * ClassSize.ARRAY);
611
612    if (expected != actual) {
613      expected = ClassSize.estimateBase(cl, true);
614      assertEquals(expected, actual);
615    }
616  }
617
618  /**
619   * to check if looks good when midKey on a leaf index block boundary
620   */
621  @Test
622  public void testMidKeyOnLeafIndexBlockBoundary() throws IOException {
623    Path hfilePath = new Path(TEST_UTIL.getDataTestDir(), "hfile_for_midkey");
624    int maxChunkSize = 512;
625    conf.setInt(HFileBlockIndex.MAX_CHUNK_SIZE_KEY, maxChunkSize);
626    // should open hfile.block.index.cacheonwrite
627    conf.setBoolean(CacheConfig.CACHE_INDEX_BLOCKS_ON_WRITE_KEY, true);
628    CacheConfig cacheConf = new CacheConfig(conf, BlockCacheFactory.createBlockCache(conf));
629    BlockCache blockCache = cacheConf.getBlockCache().get();
630    // Evict all blocks that were cached-on-write by the previous invocation.
631    blockCache.evictBlocksByHfileName(hfilePath.getName());
632    // Write the HFile
633    HFileContext meta = new HFileContextBuilder().withBlockSize(SMALL_BLOCK_SIZE)
634      .withCompression(Algorithm.NONE).withDataBlockEncoding(DataBlockEncoding.NONE).build();
635    HFile.Writer writer = HFile.getWriterFactory(conf, cacheConf).withPath(fs, hfilePath)
636      .withFileContext(meta).create();
637    Random rand = new Random(19231737);
638    byte[] family = Bytes.toBytes("f");
639    byte[] qualifier = Bytes.toBytes("q");
640    int kvNumberToBeWritten = 16;
641    // the new generated hfile will contain 2 leaf-index blocks and 16 data blocks,
642    // midkey is just on the boundary of the first leaf-index block
643    for (int i = 0; i < kvNumberToBeWritten; ++i) {
644      byte[] row = RandomKeyValueUtil.randomOrderedFixedLengthKey(rand, i, 30);
645
646      // Key will be interpreted by KeyValue.KEY_COMPARATOR
647      KeyValue kv = new KeyValue(row, family, qualifier, EnvironmentEdgeManager.currentTime(),
648        RandomKeyValueUtil.randomFixedLengthValue(rand, SMALL_BLOCK_SIZE));
649      writer.append(kv);
650    }
651    writer.close();
652
653    // close hfile.block.index.cacheonwrite
654    conf.setBoolean(CacheConfig.CACHE_INDEX_BLOCKS_ON_WRITE_KEY, false);
655
656    // Read the HFile
657    HFile.Reader reader = HFile.createReader(fs, hfilePath, cacheConf, true, conf);
658
659    boolean hasArrayIndexOutOfBoundsException = false;
660    try {
661      // get the mid-key.
662      reader.midKey();
663    } catch (ArrayIndexOutOfBoundsException e) {
664      hasArrayIndexOutOfBoundsException = true;
665    } finally {
666      reader.close();
667    }
668
669    // to check if ArrayIndexOutOfBoundsException occurred
670    assertFalse(hasArrayIndexOutOfBoundsException);
671  }
672
673  /**
674   * Testing block index through the HFile writer/reader APIs. Allows to test setting index block
675   * size through configuration, intermediate-level index blocks, and caching index blocks on write.
676   */
677  @Test
678  public void testHFileWriterAndReader() throws IOException {
679    Path hfilePath = new Path(TEST_UTIL.getDataTestDir(), "hfile_for_block_index");
680    CacheConfig cacheConf = new CacheConfig(conf, BlockCacheFactory.createBlockCache(conf));
681    BlockCache blockCache = cacheConf.getBlockCache().get();
682
683    for (int testI = 0; testI < INDEX_CHUNK_SIZES.length; ++testI) {
684      int indexBlockSize = INDEX_CHUNK_SIZES[testI];
685      int expectedNumLevels = EXPECTED_NUM_LEVELS[testI];
686      LOG.info("Index block size: " + indexBlockSize + ", compression: " + compr);
687      // Evict all blocks that were cached-on-write by the previous invocation.
688      blockCache.evictBlocksByHfileName(hfilePath.getName());
689
690      conf.setInt(HFileBlockIndex.MAX_CHUNK_SIZE_KEY, indexBlockSize);
691      Set<String> keyStrSet = new HashSet<>();
692      byte[][] keys = new byte[NUM_KV][];
693      byte[][] values = new byte[NUM_KV][];
694
695      // Write the HFile
696      {
697        HFileContext meta =
698          new HFileContextBuilder().withBlockSize(SMALL_BLOCK_SIZE).withCompression(compr).build();
699        HFile.Writer writer = HFile.getWriterFactory(conf, cacheConf).withPath(fs, hfilePath)
700          .withFileContext(meta).create();
701        Random rand = new Random(19231737);
702        byte[] family = Bytes.toBytes("f");
703        byte[] qualifier = Bytes.toBytes("q");
704        for (int i = 0; i < NUM_KV; ++i) {
705          byte[] row = RandomKeyValueUtil.randomOrderedKey(rand, i);
706
707          // Key will be interpreted by KeyValue.KEY_COMPARATOR
708          KeyValue kv = new KeyValue(row, family, qualifier, EnvironmentEdgeManager.currentTime(),
709            RandomKeyValueUtil.randomValue(rand));
710          byte[] k = kv.getKey();
711          writer.append(kv);
712          keys[i] = k;
713          values[i] = CellUtil.cloneValue(kv);
714          keyStrSet.add(Bytes.toStringBinary(k));
715          if (i > 0) {
716            assertTrue((PrivateCellUtil.compare(CellComparatorImpl.COMPARATOR, kv, keys[i - 1], 0,
717              keys[i - 1].length)) > 0);
718          }
719        }
720
721        writer.close();
722      }
723
724      // Read the HFile
725      HFile.Reader reader = HFile.createReader(fs, hfilePath, cacheConf, true, conf);
726      assertEquals(expectedNumLevels, reader.getTrailer().getNumDataIndexLevels());
727
728      assertTrue(Bytes.equals(keys[0], ((KeyValue) reader.getFirstKey().get()).getKey()));
729      assertTrue(Bytes.equals(keys[NUM_KV - 1], ((KeyValue) reader.getLastKey().get()).getKey()));
730      LOG.info("Last key: " + Bytes.toStringBinary(keys[NUM_KV - 1]));
731
732      for (boolean pread : new boolean[] { false, true }) {
733        HFileScanner scanner = reader.getScanner(conf, true, pread);
734        for (int i = 0; i < NUM_KV; ++i) {
735          checkSeekTo(keys, scanner, i);
736          checkKeyValue("i=" + i, keys[i], values[i],
737            ByteBuffer.wrap(((KeyValue) scanner.getKey()).getKey()), scanner.getValue());
738        }
739        assertTrue(scanner.seekTo());
740        for (int i = NUM_KV - 1; i >= 0; --i) {
741          checkSeekTo(keys, scanner, i);
742          checkKeyValue("i=" + i, keys[i], values[i],
743            ByteBuffer.wrap(((KeyValue) scanner.getKey()).getKey()), scanner.getValue());
744        }
745      }
746
747      // Manually compute the mid-key and validate it.
748      HFile.Reader reader2 = reader;
749      HFileBlock.FSReader fsReader = reader2.getUncachedBlockReader();
750
751      HFileBlock.BlockIterator iter =
752        fsReader.blockRange(0, reader.getTrailer().getLoadOnOpenDataOffset());
753      HFileBlock block;
754      List<byte[]> blockKeys = new ArrayList<>();
755      while ((block = iter.nextBlock()) != null) {
756        if (block.getBlockType() != BlockType.LEAF_INDEX) return;
757        ByteBuff b = block.getBufferReadOnly();
758        int n = b.getIntAfterPosition(0);
759        // One int for the number of items, and n + 1 for the secondary index.
760        int entriesOffset = Bytes.SIZEOF_INT * (n + 2);
761
762        // Get all the keys from the leaf index block. S
763        for (int i = 0; i < n; ++i) {
764          int keyRelOffset = b.getIntAfterPosition(Bytes.SIZEOF_INT * (i + 1));
765          int nextKeyRelOffset = b.getIntAfterPosition(Bytes.SIZEOF_INT * (i + 2));
766          int keyLen = nextKeyRelOffset - keyRelOffset;
767          int keyOffset = b.arrayOffset() + entriesOffset + keyRelOffset
768            + HFileBlockIndex.SECONDARY_INDEX_ENTRY_OVERHEAD;
769          byte[] blockKey = Arrays.copyOfRange(b.array(), keyOffset, keyOffset + keyLen);
770          String blockKeyStr = Bytes.toString(blockKey);
771          blockKeys.add(blockKey);
772
773          // If the first key of the block is not among the keys written, we
774          // are not parsing the non-root index block format correctly.
775          assertTrue("Invalid block key from leaf-level block: " + blockKeyStr,
776            keyStrSet.contains(blockKeyStr));
777        }
778      }
779
780      // Validate the mid-key.
781      assertEquals(Bytes.toStringBinary(blockKeys.get((blockKeys.size() - 1) / 2)),
782        reader.midKey());
783
784      assertEquals(UNCOMPRESSED_INDEX_SIZES[testI],
785        reader.getTrailer().getUncompressedDataIndexSize());
786
787      reader.close();
788      reader2.close();
789    }
790  }
791
792  private void checkSeekTo(byte[][] keys, HFileScanner scanner, int i) throws IOException {
793    assertEquals("Failed to seek to key #" + i + " (" + Bytes.toStringBinary(keys[i]) + ")", 0,
794      scanner.seekTo(KeyValueUtil.createKeyValueFromKey(keys[i])));
795  }
796
797  private void assertArrayEqualsBuffer(String msgPrefix, byte[] arr, ByteBuffer buf) {
798    assertEquals(
799      msgPrefix + ": expected " + Bytes.toStringBinary(arr) + ", actual "
800        + Bytes.toStringBinary(buf),
801      0, Bytes.compareTo(arr, 0, arr.length, buf.array(), buf.arrayOffset(), buf.limit()));
802  }
803
804  /** Check a key/value pair after it was read by the reader */
805  private void checkKeyValue(String msgPrefix, byte[] expectedKey, byte[] expectedValue,
806    ByteBuffer keyRead, ByteBuffer valueRead) {
807    if (!msgPrefix.isEmpty()) msgPrefix += ". ";
808
809    assertArrayEqualsBuffer(msgPrefix + "Invalid key", expectedKey, keyRead);
810    assertArrayEqualsBuffer(msgPrefix + "Invalid value", expectedValue, valueRead);
811  }
812
813  @Test
814  public void testIntermediateLevelIndicesWithLargeKeys() throws IOException {
815    testIntermediateLevelIndicesWithLargeKeys(16);
816  }
817
818  @Test
819  public void testIntermediateLevelIndicesWithLargeKeysWithMinNumEntries() throws IOException {
820    // because of the large rowKeys, we will end up with a 50-level block index without sanity check
821    testIntermediateLevelIndicesWithLargeKeys(2);
822  }
823
824  public void testIntermediateLevelIndicesWithLargeKeys(int minNumEntries) throws IOException {
825    Path hfPath =
826      new Path(TEST_UTIL.getDataTestDir(), "testIntermediateLevelIndicesWithLargeKeys.hfile");
827    int maxChunkSize = 1024;
828    FileSystem fs = FileSystem.get(conf);
829    CacheConfig cacheConf = new CacheConfig(conf);
830    conf.setInt(HFileBlockIndex.MAX_CHUNK_SIZE_KEY, maxChunkSize);
831    conf.setInt(HFileBlockIndex.MIN_INDEX_NUM_ENTRIES_KEY, minNumEntries);
832    HFileContext context = new HFileContextBuilder().withBlockSize(16).build();
833    HFile.Writer hfw = new HFile.WriterFactory(conf, cacheConf).withFileContext(context)
834      .withPath(fs, hfPath).create();
835    List<byte[]> keys = new ArrayList<>();
836
837    // This should result in leaf-level indices and a root level index
838    for (int i = 0; i < 100; i++) {
839      byte[] rowkey = new byte[maxChunkSize + 1];
840      byte[] b = Bytes.toBytes(i);
841      System.arraycopy(b, 0, rowkey, rowkey.length - b.length, b.length);
842      keys.add(rowkey);
843      hfw.append(ExtendedCellBuilderFactory.create(CellBuilderType.DEEP_COPY).setRow(rowkey)
844        .setFamily(HConstants.EMPTY_BYTE_ARRAY).setQualifier(HConstants.EMPTY_BYTE_ARRAY)
845        .setTimestamp(HConstants.LATEST_TIMESTAMP).setType(KeyValue.Type.Maximum.getCode())
846        .setValue(HConstants.EMPTY_BYTE_ARRAY).build());
847    }
848    hfw.close();
849
850    HFile.Reader reader = HFile.createReader(fs, hfPath, cacheConf, true, conf);
851    // Scanner doesn't do Cells yet. Fix.
852    HFileScanner scanner = reader.getScanner(conf, true, true);
853    for (int i = 0; i < keys.size(); ++i) {
854      scanner.seekTo(ExtendedCellBuilderFactory.create(CellBuilderType.DEEP_COPY)
855        .setRow(keys.get(i)).setFamily(HConstants.EMPTY_BYTE_ARRAY)
856        .setQualifier(HConstants.EMPTY_BYTE_ARRAY).setTimestamp(HConstants.LATEST_TIMESTAMP)
857        .setType(KeyValue.Type.Maximum.getCode()).setValue(HConstants.EMPTY_BYTE_ARRAY).build());
858    }
859    reader.close();
860  }
861
862  /**
863   * This test is for HBASE-27940, which midkey metadata in root index block would always be ignored
864   * by {@link BlockIndexReader#readMultiLevelIndexRoot}.
865   */
866  @Test
867  public void testMidKeyReadSuccessfullyFromRootIndexBlock() throws IOException {
868    conf.setInt(HFileBlockIndex.MAX_CHUNK_SIZE_KEY, 128);
869    Path hfilePath =
870      new Path(TEST_UTIL.getDataTestDir(), "testMidKeyReadSuccessfullyFromRootIndexBlock");
871    Compression.Algorithm compressAlgo = Compression.Algorithm.NONE;
872    int entryCount = 50000;
873    HFileContext context = new HFileContextBuilder().withBlockSize(4096).withIncludesTags(false)
874      .withDataBlockEncoding(DataBlockEncoding.NONE).withCompression(compressAlgo).build();
875
876    try (HFile.Writer writer = new HFile.WriterFactory(conf, new CacheConfig(conf))
877      .withPath(fs, hfilePath).withFileContext(context).create()) {
878
879      List<KeyValue> keyValues = new ArrayList<>(entryCount);
880      for (int i = 0; i < entryCount; ++i) {
881        byte[] keyBytes = RandomKeyValueUtil.randomOrderedKey(RNG, i);
882        // A random-length random value.
883        byte[] valueBytes = RandomKeyValueUtil.randomValue(RNG);
884        KeyValue keyValue =
885          new KeyValue(keyBytes, null, null, HConstants.LATEST_TIMESTAMP, valueBytes);
886        writer.append(keyValue);
887        keyValues.add(keyValue);
888      }
889    }
890
891    try (FSDataInputStream fsdis = fs.open(hfilePath)) {
892      long fileSize = fs.getFileStatus(hfilePath).getLen();
893      FixedFileTrailer trailer = FixedFileTrailer.readFromStream(fsdis, fileSize);
894
895      assertEquals(3, trailer.getMajorVersion());
896      assertEquals(entryCount, trailer.getEntryCount());
897      HFileContext meta = new HFileContextBuilder().withCompression(compressAlgo)
898        .withIncludesMvcc(false).withIncludesTags(false)
899        .withDataBlockEncoding(DataBlockEncoding.NONE).withHBaseCheckSum(true).build();
900      ReaderContext readerContext =
901        new ReaderContextBuilder().withInputStreamWrapper(new FSDataInputStreamWrapper(fsdis))
902          .withFilePath(hfilePath).withFileSystem(fs).withFileSize(fileSize).build();
903      HFileBlock.FSReader blockReader =
904        new HFileBlock.FSReaderImpl(readerContext, meta, ByteBuffAllocator.HEAP, conf);
905
906      MyEncoder encoder = new MyEncoder();
907      HFileBlockIndex.CellBasedKeyBlockIndexReaderV2 dataBlockIndexReader =
908        new HFileBlockIndex.CellBasedKeyBlockIndexReaderV2(trailer.createComparator(),
909          trailer.getNumDataIndexLevels(), encoder);
910
911      HFileBlock.BlockIterator blockIter = blockReader.blockRange(trailer.getLoadOnOpenDataOffset(),
912        fileSize - trailer.getTrailerSize());
913      // Data index. We also read statistics about the block index written after
914      // the root level.
915      dataBlockIndexReader.readMultiLevelIndexRoot(
916        blockIter.nextBlockWithBlockType(BlockType.ROOT_INDEX), trailer.getDataIndexCount());
917      NoOpEncodedSeeker noOpEncodedSeeker = (NoOpEncodedSeeker) encoder.encoderSeeker;
918      // Assert we have read midkey metadata successfully.
919      assertTrue(noOpEncodedSeeker.midLeafBlockOffset >= 0);
920      assertTrue(noOpEncodedSeeker.midLeafBlockOnDiskSize > 0);
921      assertTrue(noOpEncodedSeeker.midKeyEntry >= 0);
922    }
923  }
924
925  static class MyEncoder implements HFileIndexBlockEncoder {
926
927    EncodedSeeker encoderSeeker;
928
929    @Override
930    public void saveMetadata(Writer writer) throws IOException {
931      NoOpIndexBlockEncoder.INSTANCE.saveMetadata(writer);
932
933    }
934
935    @Override
936    public void encode(BlockIndexChunk blockIndexChunk, boolean rootIndexBlock, DataOutput out)
937      throws IOException {
938      NoOpIndexBlockEncoder.INSTANCE.encode(blockIndexChunk, rootIndexBlock, out);
939    }
940
941    @Override
942    public IndexBlockEncoding getIndexBlockEncoding() {
943      return NoOpIndexBlockEncoder.INSTANCE.getIndexBlockEncoding();
944    }
945
946    @Override
947    public EncodedSeeker createSeeker() {
948      encoderSeeker = NoOpIndexBlockEncoder.INSTANCE.createSeeker();
949      return encoderSeeker;
950    }
951
952  }
953}