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}