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}