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.apache.hadoop.hbase.io.compress.Compression.Algorithm.GZ; 021import static org.apache.hadoop.hbase.io.compress.Compression.Algorithm.NONE; 022import static org.junit.Assert.*; 023 024import java.io.ByteArrayOutputStream; 025import java.io.DataOutputStream; 026import java.io.IOException; 027import java.io.OutputStream; 028import java.nio.ByteBuffer; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Random; 036import java.util.concurrent.Callable; 037import java.util.concurrent.ExecutionException; 038import java.util.concurrent.Executor; 039import java.util.concurrent.ExecutorCompletionService; 040import java.util.concurrent.Executors; 041import java.util.concurrent.Future; 042import org.apache.hadoop.fs.FSDataInputStream; 043import org.apache.hadoop.fs.FSDataOutputStream; 044import org.apache.hadoop.fs.FileSystem; 045import org.apache.hadoop.fs.Path; 046import org.apache.hadoop.hbase.ArrayBackedTag; 047import org.apache.hadoop.hbase.CellComparatorImpl; 048import org.apache.hadoop.hbase.CellUtil; 049import org.apache.hadoop.hbase.HBaseClassTestRule; 050import org.apache.hadoop.hbase.HBaseTestingUtility; 051import org.apache.hadoop.hbase.HConstants; 052import org.apache.hadoop.hbase.KeyValue; 053import org.apache.hadoop.hbase.Tag; 054import org.apache.hadoop.hbase.fs.HFileSystem; 055import org.apache.hadoop.hbase.io.compress.Compression; 056import org.apache.hadoop.hbase.io.compress.Compression.Algorithm; 057import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding; 058import org.apache.hadoop.hbase.io.hfile.Cacheable.MemoryType; 059import org.apache.hadoop.hbase.nio.ByteBuff; 060import org.apache.hadoop.hbase.nio.MultiByteBuff; 061import org.apache.hadoop.hbase.nio.SingleByteBuff; 062import org.apache.hadoop.hbase.testclassification.IOTests; 063import org.apache.hadoop.hbase.testclassification.MediumTests; 064import org.apache.hadoop.hbase.util.Bytes; 065import org.apache.hadoop.hbase.util.ChecksumType; 066import org.apache.hadoop.hbase.util.ClassSize; 067import org.apache.hadoop.io.WritableUtils; 068import org.apache.hadoop.io.compress.Compressor; 069import org.junit.Before; 070import org.junit.ClassRule; 071import org.junit.Test; 072import org.junit.experimental.categories.Category; 073import org.junit.runner.RunWith; 074import org.junit.runners.Parameterized; 075import org.junit.runners.Parameterized.Parameters; 076import org.mockito.Mockito; 077import org.slf4j.Logger; 078import org.slf4j.LoggerFactory; 079 080@Category({IOTests.class, MediumTests.class}) 081@RunWith(Parameterized.class) 082public class TestHFileBlock { 083 084 @ClassRule 085 public static final HBaseClassTestRule CLASS_RULE = 086 HBaseClassTestRule.forClass(TestHFileBlock.class); 087 088 // change this value to activate more logs 089 private static final boolean detailedLogging = false; 090 private static final boolean[] BOOLEAN_VALUES = new boolean[] { false, true }; 091 092 private static final Logger LOG = LoggerFactory.getLogger(TestHFileBlock.class); 093 094 static final Compression.Algorithm[] COMPRESSION_ALGORITHMS = { NONE, GZ }; 095 096 private static final int NUM_TEST_BLOCKS = 1000; 097 private static final int NUM_READER_THREADS = 26; 098 099 // Used to generate KeyValues 100 private static int NUM_KEYVALUES = 50; 101 private static int FIELD_LENGTH = 10; 102 private static float CHANCE_TO_REPEAT = 0.6f; 103 104 private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 105 private FileSystem fs; 106 107 private final boolean includesMemstoreTS; 108 private final boolean includesTag; 109 public TestHFileBlock(boolean includesMemstoreTS, boolean includesTag) { 110 this.includesMemstoreTS = includesMemstoreTS; 111 this.includesTag = includesTag; 112 } 113 114 @Parameters 115 public static Collection<Object[]> parameters() { 116 return HBaseTestingUtility.MEMSTORETS_TAGS_PARAMETRIZED; 117 } 118 119 @Before 120 public void setUp() throws IOException { 121 fs = HFileSystem.get(TEST_UTIL.getConfiguration()); 122 } 123 124 static void writeTestBlockContents(DataOutputStream dos) throws IOException { 125 // This compresses really well. 126 for (int i = 0; i < 1000; ++i) 127 dos.writeInt(i / 100); 128 } 129 130 static int writeTestKeyValues(HFileBlock.Writer hbw, int seed, boolean includesMemstoreTS, 131 boolean useTag) throws IOException { 132 List<KeyValue> keyValues = new ArrayList<>(); 133 Random randomizer = new Random(42L + seed); // just any fixed number 134 135 // generate keyValues 136 for (int i = 0; i < NUM_KEYVALUES; ++i) { 137 byte[] row; 138 long timestamp; 139 byte[] family; 140 byte[] qualifier; 141 byte[] value; 142 143 // generate it or repeat, it should compress well 144 if (0 < i && randomizer.nextFloat() < CHANCE_TO_REPEAT) { 145 row = CellUtil.cloneRow(keyValues.get(randomizer.nextInt(keyValues.size()))); 146 } else { 147 row = new byte[FIELD_LENGTH]; 148 randomizer.nextBytes(row); 149 } 150 if (0 == i) { 151 family = new byte[FIELD_LENGTH]; 152 randomizer.nextBytes(family); 153 } else { 154 family = CellUtil.cloneFamily(keyValues.get(0)); 155 } 156 if (0 < i && randomizer.nextFloat() < CHANCE_TO_REPEAT) { 157 qualifier = CellUtil.cloneQualifier(keyValues.get(randomizer.nextInt(keyValues.size()))); 158 } else { 159 qualifier = new byte[FIELD_LENGTH]; 160 randomizer.nextBytes(qualifier); 161 } 162 if (0 < i && randomizer.nextFloat() < CHANCE_TO_REPEAT) { 163 value = CellUtil.cloneValue(keyValues.get(randomizer.nextInt(keyValues.size()))); 164 } else { 165 value = new byte[FIELD_LENGTH]; 166 randomizer.nextBytes(value); 167 } 168 if (0 < i && randomizer.nextFloat() < CHANCE_TO_REPEAT) { 169 timestamp = keyValues.get( 170 randomizer.nextInt(keyValues.size())).getTimestamp(); 171 } else { 172 timestamp = randomizer.nextLong(); 173 } 174 if (!useTag) { 175 keyValues.add(new KeyValue(row, family, qualifier, timestamp, value)); 176 } else { 177 keyValues.add(new KeyValue(row, family, qualifier, timestamp, value, 178 new Tag[] { new ArrayBackedTag((byte) 1, Bytes.toBytes("myTagVal")) })); 179 } 180 } 181 182 // sort it and write to stream 183 int totalSize = 0; 184 Collections.sort(keyValues, CellComparatorImpl.COMPARATOR); 185 186 for (KeyValue kv : keyValues) { 187 totalSize += kv.getLength(); 188 if (includesMemstoreTS) { 189 long memstoreTS = randomizer.nextLong(); 190 kv.setSequenceId(memstoreTS); 191 totalSize += WritableUtils.getVIntSize(memstoreTS); 192 } 193 hbw.write(kv); 194 } 195 return totalSize; 196 } 197 198 public byte[] createTestV1Block(Compression.Algorithm algo) 199 throws IOException { 200 Compressor compressor = algo.getCompressor(); 201 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 202 OutputStream os = algo.createCompressionStream(baos, compressor, 0); 203 DataOutputStream dos = new DataOutputStream(os); 204 BlockType.META.write(dos); // Let's make this a meta block. 205 writeTestBlockContents(dos); 206 dos.flush(); 207 algo.returnCompressor(compressor); 208 return baos.toByteArray(); 209 } 210 211 static HFileBlock.Writer createTestV2Block(Compression.Algorithm algo, 212 boolean includesMemstoreTS, boolean includesTag) throws IOException { 213 final BlockType blockType = BlockType.DATA; 214 HFileContext meta = new HFileContextBuilder() 215 .withCompression(algo) 216 .withIncludesMvcc(includesMemstoreTS) 217 .withIncludesTags(includesTag) 218 .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM) 219 .build(); 220 HFileBlock.Writer hbw = new HFileBlock.Writer(null, meta); 221 DataOutputStream dos = hbw.startWriting(blockType); 222 writeTestBlockContents(dos); 223 dos.flush(); 224 hbw.ensureBlockReady(); 225 assertEquals(1000 * 4, hbw.getUncompressedSizeWithoutHeader()); 226 hbw.release(); 227 return hbw; 228 } 229 230 public String createTestBlockStr(Compression.Algorithm algo, 231 int correctLength, boolean useTag) throws IOException { 232 HFileBlock.Writer hbw = createTestV2Block(algo, includesMemstoreTS, useTag); 233 byte[] testV2Block = hbw.getHeaderAndDataForTest(); 234 int osOffset = HConstants.HFILEBLOCK_HEADER_SIZE + 9; 235 if (testV2Block.length == correctLength) { 236 // Force-set the "OS" field of the gzip header to 3 (Unix) to avoid 237 // variations across operating systems. 238 // See http://www.gzip.org/zlib/rfc-gzip.html for gzip format. 239 // We only make this change when the compressed block length matches. 240 // Otherwise, there are obviously other inconsistencies. 241 testV2Block[osOffset] = 3; 242 } 243 return Bytes.toStringBinary(testV2Block); 244 } 245 246 @Test 247 public void testNoCompression() throws IOException { 248 CacheConfig cacheConf = Mockito.mock(CacheConfig.class); 249 Mockito.when(cacheConf.isBlockCacheEnabled()).thenReturn(false); 250 251 HFileBlock block = 252 createTestV2Block(NONE, includesMemstoreTS, false).getBlockForCaching(cacheConf); 253 assertEquals(4000, block.getUncompressedSizeWithoutHeader()); 254 assertEquals(4004, block.getOnDiskSizeWithoutHeader()); 255 assertTrue(block.isUnpacked()); 256 } 257 258 @Test 259 public void testGzipCompression() throws IOException { 260 final String correctTestBlockStr = 261 "DATABLK*\\x00\\x00\\x00>\\x00\\x00\\x0F\\xA0\\xFF\\xFF\\xFF\\xFF" 262 + "\\xFF\\xFF\\xFF\\xFF" 263 + "\\x0" + ChecksumType.getDefaultChecksumType().getCode() 264 + "\\x00\\x00@\\x00\\x00\\x00\\x00[" 265 // gzip-compressed block: http://www.gzip.org/zlib/rfc-gzip.html 266 + "\\x1F\\x8B" // gzip magic signature 267 + "\\x08" // Compression method: 8 = "deflate" 268 + "\\x00" // Flags 269 + "\\x00\\x00\\x00\\x00" // mtime 270 + "\\x00" // XFL (extra flags) 271 // OS (0 = FAT filesystems, 3 = Unix). However, this field 272 // sometimes gets set to 0 on Linux and Mac, so we reset it to 3. 273 // This appears to be a difference caused by the availability 274 // (and use) of the native GZ codec. 275 + "\\x03" 276 + "\\xED\\xC3\\xC1\\x11\\x00 \\x08\\xC00DD\\xDD\\x7Fa" 277 + "\\xD6\\xE8\\xA3\\xB9K\\x84`\\x96Q\\xD3\\xA8\\xDB\\xA8e\\xD4c" 278 + "\\xD46\\xEA5\\xEA3\\xEA7\\xE7\\x00LI\\x5Cs\\xA0\\x0F\\x00\\x00" 279 + "\\x00\\x00\\x00\\x00"; // 4 byte checksum (ignored) 280 final int correctGzipBlockLength = 95; 281 final String testBlockStr = createTestBlockStr(GZ, correctGzipBlockLength, false); 282 // We ignore the block checksum because createTestBlockStr can change the 283 // gzip header after the block is produced 284 assertEquals(correctTestBlockStr.substring(0, correctGzipBlockLength - 4), 285 testBlockStr.substring(0, correctGzipBlockLength - 4)); 286 } 287 288 @Test 289 public void testReaderV2() throws IOException { 290 testReaderV2Internals(); 291 } 292 293 protected void testReaderV2Internals() throws IOException { 294 if(includesTag) { 295 TEST_UTIL.getConfiguration().setInt("hfile.format.version", 3); 296 } 297 for (Compression.Algorithm algo : COMPRESSION_ALGORITHMS) { 298 for (boolean pread : new boolean[] { false, true }) { 299 LOG.info("testReaderV2: Compression algorithm: " + algo + 300 ", pread=" + pread); 301 Path path = new Path(TEST_UTIL.getDataTestDir(), "blocks_v2_" 302 + algo); 303 FSDataOutputStream os = fs.create(path); 304 HFileContext meta = new HFileContextBuilder() 305 .withCompression(algo) 306 .withIncludesMvcc(includesMemstoreTS) 307 .withIncludesTags(includesTag) 308 .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM) 309 .build(); 310 HFileBlock.Writer hbw = new HFileBlock.Writer(null, 311 meta); 312 long totalSize = 0; 313 for (int blockId = 0; blockId < 2; ++blockId) { 314 DataOutputStream dos = hbw.startWriting(BlockType.DATA); 315 for (int i = 0; i < 1234; ++i) 316 dos.writeInt(i); 317 hbw.writeHeaderAndData(os); 318 totalSize += hbw.getOnDiskSizeWithHeader(); 319 } 320 os.close(); 321 322 FSDataInputStream is = fs.open(path); 323 meta = new HFileContextBuilder() 324 .withHBaseCheckSum(true) 325 .withIncludesMvcc(includesMemstoreTS) 326 .withIncludesTags(includesTag) 327 .withCompression(algo).build(); 328 HFileBlock.FSReader hbr = new HFileBlock.FSReaderImpl(is, totalSize, meta); 329 HFileBlock b = hbr.readBlockData(0, -1, pread, false); 330 is.close(); 331 assertEquals(0, HFile.getAndResetChecksumFailuresCount()); 332 333 b.sanityCheck(); 334 assertEquals(4936, b.getUncompressedSizeWithoutHeader()); 335 assertEquals(algo == GZ ? 2173 : 4936, 336 b.getOnDiskSizeWithoutHeader() - b.totalChecksumBytes()); 337 HFileBlock expected = b; 338 339 if (algo == GZ) { 340 is = fs.open(path); 341 hbr = new HFileBlock.FSReaderImpl(is, totalSize, meta); 342 b = hbr.readBlockData(0, 2173 + HConstants.HFILEBLOCK_HEADER_SIZE + 343 b.totalChecksumBytes(), pread, false); 344 assertEquals(expected, b); 345 int wrongCompressedSize = 2172; 346 try { 347 b = hbr.readBlockData(0, wrongCompressedSize 348 + HConstants.HFILEBLOCK_HEADER_SIZE, pread, false); 349 fail("Exception expected"); 350 } catch (IOException ex) { 351 String expectedPrefix = "Passed in onDiskSizeWithHeader="; 352 assertTrue("Invalid exception message: '" + ex.getMessage() 353 + "'.\nMessage is expected to start with: '" + expectedPrefix 354 + "'", ex.getMessage().startsWith(expectedPrefix)); 355 } 356 is.close(); 357 } 358 } 359 } 360 } 361 362 /** 363 * Test encoding/decoding data blocks. 364 * @throws IOException a bug or a problem with temporary files. 365 */ 366 @Test 367 public void testDataBlockEncoding() throws IOException { 368 testInternals(); 369 } 370 371 private void testInternals() throws IOException { 372 final int numBlocks = 5; 373 if(includesTag) { 374 TEST_UTIL.getConfiguration().setInt("hfile.format.version", 3); 375 } 376 for (Compression.Algorithm algo : COMPRESSION_ALGORITHMS) { 377 for (boolean pread : new boolean[] { false, true }) { 378 for (DataBlockEncoding encoding : DataBlockEncoding.values()) { 379 Path path = new Path(TEST_UTIL.getDataTestDir(), "blocks_v2_" 380 + algo + "_" + encoding.toString()); 381 FSDataOutputStream os = fs.create(path); 382 HFileDataBlockEncoder dataBlockEncoder = (encoding != DataBlockEncoding.NONE) ? 383 new HFileDataBlockEncoderImpl(encoding) : NoOpDataBlockEncoder.INSTANCE; 384 HFileContext meta = new HFileContextBuilder() 385 .withCompression(algo) 386 .withIncludesMvcc(includesMemstoreTS) 387 .withIncludesTags(includesTag) 388 .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM) 389 .build(); 390 HFileBlock.Writer hbw = new HFileBlock.Writer(dataBlockEncoder, meta); 391 long totalSize = 0; 392 final List<Integer> encodedSizes = new ArrayList<>(); 393 final List<ByteBuffer> encodedBlocks = new ArrayList<>(); 394 for (int blockId = 0; blockId < numBlocks; ++blockId) { 395 hbw.startWriting(BlockType.DATA); 396 writeTestKeyValues(hbw, blockId, includesMemstoreTS, includesTag); 397 hbw.writeHeaderAndData(os); 398 int headerLen = HConstants.HFILEBLOCK_HEADER_SIZE; 399 byte[] encodedResultWithHeader = hbw.cloneUncompressedBufferWithHeader().array(); 400 final int encodedSize = encodedResultWithHeader.length - headerLen; 401 if (encoding != DataBlockEncoding.NONE) { 402 // We need to account for the two-byte encoding algorithm ID that 403 // comes after the 24-byte block header but before encoded KVs. 404 headerLen += DataBlockEncoding.ID_SIZE; 405 } 406 byte[] encodedDataSection = 407 new byte[encodedResultWithHeader.length - headerLen]; 408 System.arraycopy(encodedResultWithHeader, headerLen, 409 encodedDataSection, 0, encodedDataSection.length); 410 final ByteBuffer encodedBuf = 411 ByteBuffer.wrap(encodedDataSection); 412 encodedSizes.add(encodedSize); 413 encodedBlocks.add(encodedBuf); 414 totalSize += hbw.getOnDiskSizeWithHeader(); 415 } 416 os.close(); 417 418 FSDataInputStream is = fs.open(path); 419 meta = new HFileContextBuilder() 420 .withHBaseCheckSum(true) 421 .withCompression(algo) 422 .withIncludesMvcc(includesMemstoreTS) 423 .withIncludesTags(includesTag) 424 .build(); 425 HFileBlock.FSReaderImpl hbr = new HFileBlock.FSReaderImpl(is, totalSize, meta); 426 hbr.setDataBlockEncoder(dataBlockEncoder); 427 hbr.setIncludesMemStoreTS(includesMemstoreTS); 428 HFileBlock blockFromHFile, blockUnpacked; 429 int pos = 0; 430 for (int blockId = 0; blockId < numBlocks; ++blockId) { 431 blockFromHFile = hbr.readBlockData(pos, -1, pread, false); 432 assertEquals(0, HFile.getAndResetChecksumFailuresCount()); 433 blockFromHFile.sanityCheck(); 434 pos += blockFromHFile.getOnDiskSizeWithHeader(); 435 assertEquals((int) encodedSizes.get(blockId), 436 blockFromHFile.getUncompressedSizeWithoutHeader()); 437 assertEquals(meta.isCompressedOrEncrypted(), !blockFromHFile.isUnpacked()); 438 long packedHeapsize = blockFromHFile.heapSize(); 439 blockUnpacked = blockFromHFile.unpack(meta, hbr); 440 assertTrue(blockUnpacked.isUnpacked()); 441 if (meta.isCompressedOrEncrypted()) { 442 LOG.info("packedHeapsize=" + packedHeapsize + ", unpackedHeadsize=" + blockUnpacked 443 .heapSize()); 444 assertFalse(packedHeapsize == blockUnpacked.heapSize()); 445 assertTrue("Packed heapSize should be < unpacked heapSize", 446 packedHeapsize < blockUnpacked.heapSize()); 447 } 448 ByteBuff actualBuffer = blockUnpacked.getBufferWithoutHeader(); 449 if (encoding != DataBlockEncoding.NONE) { 450 // We expect a two-byte big-endian encoding id. 451 assertEquals( 452 "Unexpected first byte with " + buildMessageDetails(algo, encoding, pread), 453 Long.toHexString(0), Long.toHexString(actualBuffer.get(0))); 454 assertEquals( 455 "Unexpected second byte with " + buildMessageDetails(algo, encoding, pread), 456 Long.toHexString(encoding.getId()), Long.toHexString(actualBuffer.get(1))); 457 actualBuffer.position(2); 458 actualBuffer = actualBuffer.slice(); 459 } 460 461 ByteBuffer expectedBuffer = encodedBlocks.get(blockId); 462 expectedBuffer.rewind(); 463 464 // test if content matches, produce nice message 465 assertBuffersEqual(new SingleByteBuff(expectedBuffer), actualBuffer, algo, encoding, 466 pread); 467 468 // test serialized blocks 469 for (boolean reuseBuffer : new boolean[] { false, true }) { 470 ByteBuffer serialized = ByteBuffer.allocate(blockFromHFile.getSerializedLength()); 471 blockFromHFile.serialize(serialized, true); 472 HFileBlock deserialized = 473 (HFileBlock) blockFromHFile.getDeserializer().deserialize( 474 new SingleByteBuff(serialized), reuseBuffer, MemoryType.EXCLUSIVE); 475 assertEquals( 476 "Serialization did not preserve block state. reuseBuffer=" + reuseBuffer, 477 blockFromHFile, deserialized); 478 // intentional reference comparison 479 if (blockFromHFile != blockUnpacked) { 480 assertEquals("Deserializaed block cannot be unpacked correctly.", 481 blockUnpacked, deserialized.unpack(meta, hbr)); 482 } 483 } 484 } 485 is.close(); 486 } 487 } 488 } 489 } 490 491 static String buildMessageDetails(Algorithm compression, DataBlockEncoding encoding, 492 boolean pread) { 493 return String.format("compression %s, encoding %s, pread %s", compression, encoding, pread); 494 } 495 496 static void assertBuffersEqual(ByteBuff expectedBuffer, 497 ByteBuff actualBuffer, Compression.Algorithm compression, 498 DataBlockEncoding encoding, boolean pread) { 499 if (!actualBuffer.equals(expectedBuffer)) { 500 int prefix = 0; 501 int minLimit = Math.min(expectedBuffer.limit(), actualBuffer.limit()); 502 while (prefix < minLimit && 503 expectedBuffer.get(prefix) == actualBuffer.get(prefix)) { 504 prefix++; 505 } 506 507 fail(String.format( 508 "Content mismatch for %s, commonPrefix %d, expected %s, got %s", 509 buildMessageDetails(compression, encoding, pread), prefix, 510 nextBytesToStr(expectedBuffer, prefix), 511 nextBytesToStr(actualBuffer, prefix))); 512 } 513 } 514 515 /** 516 * Convert a few next bytes in the given buffer at the given position to 517 * string. Used for error messages. 518 */ 519 private static String nextBytesToStr(ByteBuff buf, int pos) { 520 int maxBytes = buf.limit() - pos; 521 int numBytes = Math.min(16, maxBytes); 522 return Bytes.toStringBinary(buf.array(), buf.arrayOffset() + pos, 523 numBytes) + (numBytes < maxBytes ? "..." : ""); 524 } 525 526 @Test 527 public void testPreviousOffset() throws IOException { 528 testPreviousOffsetInternals(); 529 } 530 531 protected void testPreviousOffsetInternals() throws IOException { 532 // TODO: parameterize these nested loops. 533 for (Compression.Algorithm algo : COMPRESSION_ALGORITHMS) { 534 for (boolean pread : BOOLEAN_VALUES) { 535 for (boolean cacheOnWrite : BOOLEAN_VALUES) { 536 Random rand = defaultRandom(); 537 LOG.info("testPreviousOffset:Compression algorithm: " + algo + 538 ", pread=" + pread + 539 ", cacheOnWrite=" + cacheOnWrite); 540 Path path = new Path(TEST_UTIL.getDataTestDir(), "prev_offset"); 541 List<Long> expectedOffsets = new ArrayList<>(); 542 List<Long> expectedPrevOffsets = new ArrayList<>(); 543 List<BlockType> expectedTypes = new ArrayList<>(); 544 List<ByteBuffer> expectedContents = cacheOnWrite ? new ArrayList<>() : null; 545 long totalSize = writeBlocks(rand, algo, path, expectedOffsets, 546 expectedPrevOffsets, expectedTypes, expectedContents); 547 548 FSDataInputStream is = fs.open(path); 549 HFileContext meta = new HFileContextBuilder() 550 .withHBaseCheckSum(true) 551 .withIncludesMvcc(includesMemstoreTS) 552 .withIncludesTags(includesTag) 553 .withCompression(algo).build(); 554 HFileBlock.FSReader hbr = new HFileBlock.FSReaderImpl(is, totalSize, meta); 555 long curOffset = 0; 556 for (int i = 0; i < NUM_TEST_BLOCKS; ++i) { 557 if (!pread) { 558 assertEquals(is.getPos(), curOffset + (i == 0 ? 0 : 559 HConstants.HFILEBLOCK_HEADER_SIZE)); 560 } 561 562 assertEquals(expectedOffsets.get(i).longValue(), curOffset); 563 if (detailedLogging) { 564 LOG.info("Reading block #" + i + " at offset " + curOffset); 565 } 566 HFileBlock b = hbr.readBlockData(curOffset, -1, pread, false); 567 if (detailedLogging) { 568 LOG.info("Block #" + i + ": " + b); 569 } 570 assertEquals("Invalid block #" + i + "'s type:", 571 expectedTypes.get(i), b.getBlockType()); 572 assertEquals("Invalid previous block offset for block " + i 573 + " of " + "type " + b.getBlockType() + ":", 574 (long) expectedPrevOffsets.get(i), b.getPrevBlockOffset()); 575 b.sanityCheck(); 576 assertEquals(curOffset, b.getOffset()); 577 578 // Now re-load this block knowing the on-disk size. This tests a 579 // different branch in the loader. 580 HFileBlock b2 = hbr.readBlockData(curOffset, b.getOnDiskSizeWithHeader(), pread, false); 581 b2.sanityCheck(); 582 583 assertEquals(b.getBlockType(), b2.getBlockType()); 584 assertEquals(b.getOnDiskSizeWithoutHeader(), 585 b2.getOnDiskSizeWithoutHeader()); 586 assertEquals(b.getOnDiskSizeWithHeader(), 587 b2.getOnDiskSizeWithHeader()); 588 assertEquals(b.getUncompressedSizeWithoutHeader(), 589 b2.getUncompressedSizeWithoutHeader()); 590 assertEquals(b.getPrevBlockOffset(), b2.getPrevBlockOffset()); 591 assertEquals(curOffset, b2.getOffset()); 592 assertEquals(b.getBytesPerChecksum(), b2.getBytesPerChecksum()); 593 assertEquals(b.getOnDiskDataSizeWithHeader(), 594 b2.getOnDiskDataSizeWithHeader()); 595 assertEquals(0, HFile.getAndResetChecksumFailuresCount()); 596 597 curOffset += b.getOnDiskSizeWithHeader(); 598 599 if (cacheOnWrite) { 600 // NOTE: cache-on-write testing doesn't actually involve a BlockCache. It simply 601 // verifies that the unpacked value read back off disk matches the unpacked value 602 // generated before writing to disk. 603 b = b.unpack(meta, hbr); 604 // b's buffer has header + data + checksum while 605 // expectedContents have header + data only 606 ByteBuff bufRead = b.getBufferReadOnly(); 607 ByteBuffer bufExpected = expectedContents.get(i); 608 boolean bytesAreCorrect = Bytes.compareTo(bufRead.array(), 609 bufRead.arrayOffset(), 610 bufRead.limit() - b.totalChecksumBytes(), 611 bufExpected.array(), bufExpected.arrayOffset(), 612 bufExpected.limit()) == 0; 613 String wrongBytesMsg = ""; 614 615 if (!bytesAreCorrect) { 616 // Optimization: only construct an error message in case we 617 // will need it. 618 wrongBytesMsg = "Expected bytes in block #" + i + " (algo=" 619 + algo + ", pread=" + pread 620 + ", cacheOnWrite=" + cacheOnWrite + "):\n"; 621 wrongBytesMsg += Bytes.toStringBinary(bufExpected.array(), 622 bufExpected.arrayOffset(), Math.min(32 + 10, bufExpected.limit())) 623 + ", actual:\n" 624 + Bytes.toStringBinary(bufRead.array(), 625 bufRead.arrayOffset(), Math.min(32 + 10, bufRead.limit())); 626 if (detailedLogging) { 627 LOG.warn("expected header" + 628 HFileBlock.toStringHeader(new SingleByteBuff(bufExpected)) + 629 "\nfound header" + 630 HFileBlock.toStringHeader(bufRead)); 631 LOG.warn("bufread offset " + bufRead.arrayOffset() + 632 " limit " + bufRead.limit() + 633 " expected offset " + bufExpected.arrayOffset() + 634 " limit " + bufExpected.limit()); 635 LOG.warn(wrongBytesMsg); 636 } 637 } 638 assertTrue(wrongBytesMsg, bytesAreCorrect); 639 } 640 } 641 642 assertEquals(curOffset, fs.getFileStatus(path).getLen()); 643 is.close(); 644 } 645 } 646 } 647 } 648 649 private Random defaultRandom() { 650 return new Random(189237); 651 } 652 653 private class BlockReaderThread implements Callable<Boolean> { 654 private final String clientId; 655 private final HFileBlock.FSReader hbr; 656 private final List<Long> offsets; 657 private final List<BlockType> types; 658 private final long fileSize; 659 660 public BlockReaderThread(String clientId, 661 HFileBlock.FSReader hbr, List<Long> offsets, List<BlockType> types, 662 long fileSize) { 663 this.clientId = clientId; 664 this.offsets = offsets; 665 this.hbr = hbr; 666 this.types = types; 667 this.fileSize = fileSize; 668 } 669 670 @Override 671 public Boolean call() throws Exception { 672 Random rand = new Random(clientId.hashCode()); 673 long endTime = System.currentTimeMillis() + 10000; 674 int numBlocksRead = 0; 675 int numPositionalRead = 0; 676 int numWithOnDiskSize = 0; 677 while (System.currentTimeMillis() < endTime) { 678 int blockId = rand.nextInt(NUM_TEST_BLOCKS); 679 long offset = offsets.get(blockId); 680 // now we only support concurrent read with pread = true 681 boolean pread = true; 682 boolean withOnDiskSize = rand.nextBoolean(); 683 long expectedSize = 684 (blockId == NUM_TEST_BLOCKS - 1 ? fileSize 685 : offsets.get(blockId + 1)) - offset; 686 687 HFileBlock b; 688 try { 689 long onDiskSizeArg = withOnDiskSize ? expectedSize : -1; 690 b = hbr.readBlockData(offset, onDiskSizeArg, pread, false); 691 } catch (IOException ex) { 692 LOG.error("Error in client " + clientId + " trying to read block at " 693 + offset + ", pread=" + pread + ", withOnDiskSize=" + 694 withOnDiskSize, ex); 695 return false; 696 } 697 698 assertEquals(types.get(blockId), b.getBlockType()); 699 assertEquals(expectedSize, b.getOnDiskSizeWithHeader()); 700 assertEquals(offset, b.getOffset()); 701 702 ++numBlocksRead; 703 if (pread) 704 ++numPositionalRead; 705 if (withOnDiskSize) 706 ++numWithOnDiskSize; 707 } 708 LOG.info("Client " + clientId + " successfully read " + numBlocksRead + 709 " blocks (with pread: " + numPositionalRead + ", with onDiskSize " + 710 "specified: " + numWithOnDiskSize + ")"); 711 712 return true; 713 } 714 715 } 716 717 @Test 718 public void testConcurrentReading() throws Exception { 719 testConcurrentReadingInternals(); 720 } 721 722 protected void testConcurrentReadingInternals() throws IOException, 723 InterruptedException, ExecutionException { 724 for (Compression.Algorithm compressAlgo : COMPRESSION_ALGORITHMS) { 725 Path path = new Path(TEST_UTIL.getDataTestDir(), "concurrent_reading"); 726 Random rand = defaultRandom(); 727 List<Long> offsets = new ArrayList<>(); 728 List<BlockType> types = new ArrayList<>(); 729 writeBlocks(rand, compressAlgo, path, offsets, null, types, null); 730 FSDataInputStream is = fs.open(path); 731 long fileSize = fs.getFileStatus(path).getLen(); 732 HFileContext meta = new HFileContextBuilder() 733 .withHBaseCheckSum(true) 734 .withIncludesMvcc(includesMemstoreTS) 735 .withIncludesTags(includesTag) 736 .withCompression(compressAlgo) 737 .build(); 738 HFileBlock.FSReader hbr = new HFileBlock.FSReaderImpl(is, fileSize, meta); 739 740 Executor exec = Executors.newFixedThreadPool(NUM_READER_THREADS); 741 ExecutorCompletionService<Boolean> ecs = new ExecutorCompletionService<>(exec); 742 743 for (int i = 0; i < NUM_READER_THREADS; ++i) { 744 ecs.submit(new BlockReaderThread("reader_" + (char) ('A' + i), hbr, 745 offsets, types, fileSize)); 746 } 747 748 for (int i = 0; i < NUM_READER_THREADS; ++i) { 749 Future<Boolean> result = ecs.take(); 750 assertTrue(result.get()); 751 if (detailedLogging) { 752 LOG.info(String.valueOf(i + 1) 753 + " reader threads finished successfully (algo=" + compressAlgo 754 + ")"); 755 } 756 } 757 758 is.close(); 759 } 760 } 761 762 private long writeBlocks(Random rand, Compression.Algorithm compressAlgo, 763 Path path, List<Long> expectedOffsets, List<Long> expectedPrevOffsets, 764 List<BlockType> expectedTypes, List<ByteBuffer> expectedContents 765 ) throws IOException { 766 boolean cacheOnWrite = expectedContents != null; 767 FSDataOutputStream os = fs.create(path); 768 HFileContext meta = new HFileContextBuilder() 769 .withHBaseCheckSum(true) 770 .withIncludesMvcc(includesMemstoreTS) 771 .withIncludesTags(includesTag) 772 .withCompression(compressAlgo) 773 .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM) 774 .build(); 775 HFileBlock.Writer hbw = new HFileBlock.Writer(null, meta); 776 Map<BlockType, Long> prevOffsetByType = new HashMap<>(); 777 long totalSize = 0; 778 for (int i = 0; i < NUM_TEST_BLOCKS; ++i) { 779 long pos = os.getPos(); 780 int blockTypeOrdinal = rand.nextInt(BlockType.values().length); 781 if (blockTypeOrdinal == BlockType.ENCODED_DATA.ordinal()) { 782 blockTypeOrdinal = BlockType.DATA.ordinal(); 783 } 784 BlockType bt = BlockType.values()[blockTypeOrdinal]; 785 DataOutputStream dos = hbw.startWriting(bt); 786 int size = rand.nextInt(500); 787 for (int j = 0; j < size; ++j) { 788 // This might compress well. 789 dos.writeShort(i + 1); 790 dos.writeInt(j + 1); 791 } 792 793 if (expectedOffsets != null) 794 expectedOffsets.add(os.getPos()); 795 796 if (expectedPrevOffsets != null) { 797 Long prevOffset = prevOffsetByType.get(bt); 798 expectedPrevOffsets.add(prevOffset != null ? prevOffset : -1); 799 prevOffsetByType.put(bt, os.getPos()); 800 } 801 802 expectedTypes.add(bt); 803 804 hbw.writeHeaderAndData(os); 805 totalSize += hbw.getOnDiskSizeWithHeader(); 806 807 if (cacheOnWrite) 808 expectedContents.add(hbw.cloneUncompressedBufferWithHeader()); 809 810 if (detailedLogging) { 811 LOG.info("Written block #" + i + " of type " + bt 812 + ", uncompressed size " + hbw.getUncompressedSizeWithoutHeader() 813 + ", packed size " + hbw.getOnDiskSizeWithoutHeader() 814 + " at offset " + pos); 815 } 816 } 817 os.close(); 818 LOG.info("Created a temporary file at " + path + ", " 819 + fs.getFileStatus(path).getLen() + " byte, compression=" + 820 compressAlgo); 821 return totalSize; 822 } 823 824 @Test 825 public void testBlockHeapSize() { 826 testBlockHeapSizeInternals(); 827 } 828 829 protected void testBlockHeapSizeInternals() { 830 if (ClassSize.is32BitJVM()) { 831 assertEquals(64, HFileBlock.MULTI_BYTE_BUFFER_HEAP_SIZE); 832 } else { 833 assertEquals(72, HFileBlock.MULTI_BYTE_BUFFER_HEAP_SIZE); 834 } 835 836 for (int size : new int[] { 100, 256, 12345 }) { 837 byte[] byteArr = new byte[HConstants.HFILEBLOCK_HEADER_SIZE + size]; 838 ByteBuffer buf = ByteBuffer.wrap(byteArr, 0, size); 839 HFileContext meta = new HFileContextBuilder() 840 .withIncludesMvcc(includesMemstoreTS) 841 .withIncludesTags(includesTag) 842 .withHBaseCheckSum(false) 843 .withCompression(Algorithm.NONE) 844 .withBytesPerCheckSum(HFile.DEFAULT_BYTES_PER_CHECKSUM) 845 .withChecksumType(ChecksumType.NULL).build(); 846 HFileBlock block = new HFileBlock(BlockType.DATA, size, size, -1, buf, 847 HFileBlock.FILL_HEADER, -1, 0, -1, meta); 848 long byteBufferExpectedSize = ClassSize.align(ClassSize.estimateBase( 849 new MultiByteBuff(buf).getClass(), true) 850 + HConstants.HFILEBLOCK_HEADER_SIZE + size); 851 long hfileMetaSize = ClassSize.align(ClassSize.estimateBase(HFileContext.class, true)); 852 long hfileBlockExpectedSize = 853 ClassSize.align(ClassSize.estimateBase(HFileBlock.class, true)); 854 long expected = hfileBlockExpectedSize + byteBufferExpectedSize + hfileMetaSize; 855 assertEquals("Block data size: " + size + ", byte buffer expected " + 856 "size: " + byteBufferExpectedSize + ", HFileBlock class expected " + 857 "size: " + hfileBlockExpectedSize + ";", expected, 858 block.heapSize()); 859 } 860 } 861 862 @Test 863 public void testSerializeWithoutNextBlockMetadata() { 864 int size = 100; 865 int length = HConstants.HFILEBLOCK_HEADER_SIZE + size; 866 byte[] byteArr = new byte[length]; 867 ByteBuffer buf = ByteBuffer.wrap(byteArr, 0, size); 868 HFileContext meta = new HFileContextBuilder().build(); 869 HFileBlock blockWithNextBlockMetadata = new HFileBlock(BlockType.DATA, size, size, -1, buf, 870 HFileBlock.FILL_HEADER, -1, 52, -1, meta); 871 HFileBlock blockWithoutNextBlockMetadata = new HFileBlock(BlockType.DATA, size, size, -1, buf, 872 HFileBlock.FILL_HEADER, -1, -1, -1, meta); 873 ByteBuffer buff1 = ByteBuffer.allocate(length); 874 ByteBuffer buff2 = ByteBuffer.allocate(length); 875 blockWithNextBlockMetadata.serialize(buff1, true); 876 blockWithoutNextBlockMetadata.serialize(buff2, true); 877 assertNotEquals(buff1, buff2); 878 buff1.clear(); 879 buff2.clear(); 880 blockWithNextBlockMetadata.serialize(buff1, false); 881 blockWithoutNextBlockMetadata.serialize(buff2, false); 882 assertEquals(buff1, buff2); 883 } 884}