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