This is an automated email from the ASF dual-hosted git repository.
voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 888f6ecf7606 fix(storage-format): fix multi-block previous-block
offset and add byte-level HFile writer tests (#19083)
888f6ecf7606 is described below
commit 888f6ecf7606dc759cbb025a6b2552b5e4cf9b9f
Author: Y Ethan Guo <[email protected]>
AuthorDate: Tue Aug 11 03:39:28 2026 -0700
fix(storage-format): fix multi-block previous-block offset and add
byte-level HFile writer tests (#19083)
---
hudi-io/hfile_format.md | 16 +-
.../java/org/apache/hudi/io/hfile/HFileBlock.java | 15 -
.../apache/hudi/io/hfile/HFileFileInfoBlock.java | 23 +-
.../org/apache/hudi/io/hfile/HFileIndexBlock.java | 36 ++
.../apache/hudi/io/hfile/HFileMetaIndexBlock.java | 10 +-
.../apache/hudi/io/hfile/HFileRootIndexBlock.java | 5 +-
.../org/apache/hudi/io/hfile/HFileWriterImpl.java | 8 +-
.../main/java/org/apache/hudi/io/util/IOUtils.java | 36 --
.../hudi/io/hfile/TestHFileReadCompatibility.java | 27 ++
.../org/apache/hudi/io/hfile/TestHFileWriter.java | 478 +++++++++++++++++----
.../java/org/apache/hudi/io/util/TestIOUtils.java | 33 --
11 files changed, 506 insertions(+), 181 deletions(-)
diff --git a/hudi-io/hfile_format.md b/hudi-io/hfile_format.md
index df96148b9f10..3687c6fbc007 100644
--- a/hudi-io/hfile_format.md
+++ b/hudi-io/hfile_format.md
@@ -258,8 +258,11 @@ Each block index entry, referencing one relevant Data or
Meta Block, has the fol
- **Block Offset**: 8 bytes, long, the start offset of a data or meta block in
the file.
- **Block Size on Disk**: 4 bytes, integer, the on-disk size of the block, so
the block can be skipped based on the
size.
-- **Key Length**: [variable-length
encoded](https://en.wikipedia.org/wiki/Variable-length_quantity) number
representing
- the length of the "Key" part.
+- **Key Length**: the length of the "Key" part, encoded as a Hadoop
`WritableUtils` variable-length integer (VInt). A
+ value in `[-112, 127]` is a single byte; otherwise the first byte encodes
the number of following big-endian value
+ bytes and the sign. This is the encoding the HBase HFile block index uses
and that the reader decodes. It is **not**
+ the Protobuf varint used for the trailer and file info length prefixes (see
those sections): the two are byte-identical
+ for `0..127` but diverge at `>= 128`, so they are not interchangeable.
Key:
@@ -314,7 +317,9 @@ The "Data" part of the File Info Block has the following
format:
```
- **PBUF Magic**: 4 bytes, magic bytes `PBUF` indicating the block is using
Protobuf for serde.
-- **File Info**: a small key-value map of metadata serialized in Protobuf.
+- **File Info**: a small key-value map of metadata serialized in Protobuf,
length-delimited (a Protobuf varint length
+ prefix followed by the `InfoProto` message, i.e. `writeDelimitedTo` /
`parseDelimitedFrom`). This Protobuf varint is a
+ different encoding from the Hadoop `WritableUtils` VInt used for the
block-index Key Length above.
Here's the definition of the File Info proto `InfoProto`:
@@ -358,7 +363,10 @@ follows:
```
- **Block Magic**: 8 bytes, a sequence of bytes indicating the Trailer, i.e.,
`TRABLK"$`.
-- **Trailer Content**: the metadata fields are serialized in Protobuf, defined
as follows
+- **Trailer Content**: the metadata fields are serialized in Protobuf,
length-delimited (a Protobuf varint length prefix
+ followed by the `TrailerProto` message, i.e. `writeDelimitedTo` /
`parseDelimitedFrom`) right after the magic. As with
+ the file info block, this Protobuf varint differs from the Hadoop
`WritableUtils` VInt used for the block-index Key
+ Length. The proto is defined as follows
```
message TrailerProto {
diff --git a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileBlock.java
index 996af2759cdd..ae99ef2f1cf7 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileBlock.java
@@ -22,7 +22,6 @@ package org.apache.hudi.io.hfile;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.io.compress.CompressionCodec;
-import com.google.protobuf.CodedOutputStream;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@@ -359,18 +358,4 @@ public abstract class HFileBlock {
out.writeLong(LATEST_TIMESTAMP); // timestamp
out.write(KEY_TYPE_PUT); // key type
}
-
- /**
- * Returns the bytes of the variable length encoding for an integer.
- * @param length an integer, normally representing a length.
- * @return variable length encoding.
- * @throws IOException upon error.
- */
- static byte[] getVariableLengthEncodedBytes(int length) throws IOException {
- ByteArrayOutputStream varintBuffer = new ByteArrayOutputStream();
- CodedOutputStream varintOutput =
CodedOutputStream.newInstance(varintBuffer);
- varintOutput.writeUInt32NoTag(length);
- varintOutput.flush();
- return varintBuffer.toByteArray();
- }
}
diff --git
a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileFileInfoBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileFileInfoBlock.java
index 2fc1da4f7429..c625a73cb8ec 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileFileInfoBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileFileInfoBlock.java
@@ -23,6 +23,7 @@ import
org.apache.hudi.io.hfile.protobuf.generated.HFileProtos;
import org.apache.hudi.io.util.IOUtils;
import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -103,11 +104,31 @@ public class HFileFileInfoBlock extends HFileBlock {
outputStream.write(PB_MAGIC);
byte[] payload = builder.build().toByteArray();
try {
- outputStream.write(getVariableLengthEncodedBytes(payload.length));
+ outputStream.write(getProtobufVarIntBytes(payload.length));
} catch (IOException e) {
throw new RuntimeException("Failed to calculate File Info variable
length");
}
outputStream.write(payload);
return ByteBuffer.wrap(outputStream.toByteArray());
}
+
+ /**
+ * Encodes an integer as a Protobuf varint (base-128, little-endian with MSB
continuation bits,
+ * via {@link CodedOutputStream#writeUInt32NoTag}): the length framing
Protobuf's
+ * {@code writeDelimitedTo} produces. It is used for the file info and
trailer length prefixes,
+ * which the reader decodes with {@code parseDelimitedFrom}. This is NOT
interchangeable with the
+ * Hadoop {@code WritableUtils} VInt used by the block index
+ * ({@link HFileIndexBlock#getVarIntBytes(int)}): the two diverge for values
>= 128.
+ *
+ * @param value the integer value to encode.
+ * @return the Protobuf varint encoding.
+ * @throws IOException upon error.
+ */
+ static byte[] getProtobufVarIntBytes(int value) throws IOException {
+ ByteArrayOutputStream varIntBuffer = new ByteArrayOutputStream();
+ CodedOutputStream varIntOutput =
CodedOutputStream.newInstance(varIntBuffer);
+ varIntOutput.writeUInt32NoTag(value);
+ varIntOutput.flush();
+ return varIntBuffer.toByteArray();
+ }
}
diff --git
a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileIndexBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileIndexBlock.java
index 72ddd6edf218..e212c8488a00 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileIndexBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileIndexBlock.java
@@ -52,4 +52,40 @@ public abstract class HFileIndexBlock extends HFileBlock {
public boolean isEmpty() {
return entries.isEmpty();
}
+
+ /**
+ * Encodes an integer using {@code WritableUtils} variable-length encoding
(VInt): the encoding
+ * the HFile block index uses for the per-entry key length and that the
reader decodes
+ * ({@link org.apache.hudi.io.util.IOUtils#readVarLong}). Values in [-112,
127] use a single
+ * byte; otherwise the first byte carries the number of following big-endian
value bytes and the
+ * sign. This is NOT interchangeable with the Protobuf varint
+ * ({@link HFileFileInfoBlock#getProtobufVarIntBytes(int)}): the two diverge
for values >= 128.
+ *
+ * @param value the integer value to encode.
+ * @return the encoded byte array.
+ */
+ static byte[] getVarIntBytes(int value) {
+ if (value >= -112 && value <= 127) {
+ return new byte[] {(byte) value};
+ }
+ long longValue = value;
+ int len = -112;
+ if (longValue < 0) {
+ longValue ^= -1L;
+ len = -120;
+ }
+ long tmp = longValue;
+ while (tmp != 0) {
+ tmp >>= 8;
+ len--;
+ }
+ int numBytes = (len < -120) ? -(len + 120) : -(len + 112);
+ byte[] result = new byte[1 + numBytes];
+ result[0] = (byte) len;
+ for (int idx = 0; idx < numBytes; idx++) {
+ int shiftBits = (numBytes - idx - 1) * 8;
+ result[1 + idx] = (byte) ((longValue >> shiftBits) & 0xFF);
+ }
+ return result;
+ }
}
diff --git
a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileMetaIndexBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileMetaIndexBlock.java
index 72f720b9c494..e461451c1195 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileMetaIndexBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileMetaIndexBlock.java
@@ -24,8 +24,6 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
-import static org.apache.hudi.io.util.IOUtils.writeVarInt;
-
public class HFileMetaIndexBlock extends HFileIndexBlock {
private HFileMetaIndexBlock(HFileContext context) {
@@ -43,12 +41,8 @@ public class HFileMetaIndexBlock extends HFileIndexBlock {
for (BlockIndexEntry entry : entries) {
outputStream.writeLong(entry.getOffset());
outputStream.writeInt(entry.getSize());
- // Key length.
- // Use Hadoop WritableUtils VarInt encoding to match HBase's HFile
format.
- byte[] keyLength = writeVarInt(entry.getFirstKey().getLength());
- outputStream.write(keyLength);
- // Note that: NO two-bytes for encoding key length.
- // Key.
+ outputStream.write(getVarIntBytes(entry.getFirstKey().getLength()));
+ // Unlike the data index, the meta key is stored as-is with no 2-byte
row-length prefix.
outputStream.write(entry.getFirstKey().getBytes());
}
}
diff --git
a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileRootIndexBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileRootIndexBlock.java
index 56a06ed1ba32..e9b8cb715bf0 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileRootIndexBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileRootIndexBlock.java
@@ -34,7 +34,6 @@ import static
org.apache.hudi.io.util.IOUtils.decodeVarLongSizeOnDisk;
import static org.apache.hudi.io.util.IOUtils.readInt;
import static org.apache.hudi.io.util.IOUtils.readLong;
import static org.apache.hudi.io.util.IOUtils.readVarLong;
-import static org.apache.hudi.io.util.IOUtils.writeVarInt;
/**
* Represents a {@link HFileBlockType#ROOT_INDEX} block.
@@ -107,10 +106,8 @@ public class HFileRootIndexBlock extends HFileIndexBlock {
for (BlockIndexEntry entry : entries) {
outputStream.writeLong(entry.getOffset());
outputStream.writeInt(entry.getSize());
-
- // Use Hadoop WritableUtils VarInt encoding to match HBase's HFile
format and the reader.
int kvKeyLength = keyValueKeyLength(entry.getFirstKey().getLength());
- outputStream.write(writeVarInt(kvKeyLength));
+ outputStream.write(getVarIntBytes(kvKeyLength));
Key firstKey = entry.getFirstKey();
writeKey(outputStream, firstKey.getBytes(), firstKey.getOffset(),
firstKey.getLength());
}
diff --git
a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileWriterImpl.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileWriterImpl.java
index 5620bba7c811..348d1b06b661 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileWriterImpl.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileWriterImpl.java
@@ -31,8 +31,8 @@ import java.util.HashMap;
import java.util.Map;
import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT16;
-import static
org.apache.hudi.io.hfile.HFileBlock.getVariableLengthEncodedBytes;
import static org.apache.hudi.io.hfile.HFileBlockType.TRAILER;
+import static
org.apache.hudi.io.hfile.HFileFileInfoBlock.getProtobufVarIntBytes;
import static
org.apache.hudi.io.hfile.HFileInfo.KEY_VALUE_VERSION_WITH_MVCC_TS;
import static org.apache.hudi.io.hfile.HFileInfo.LAST_KEY;
import static org.apache.hudi.io.hfile.HFileInfo.MAX_MVCC_TS_KEY;
@@ -144,8 +144,8 @@ public class HFileWriterImpl implements HFileWriter {
// 3. Create an index entry.
rootIndexBlock.add(
currentDataBlock.getFirstKey(), lastDataBlockOffset,
blockBuffer.limit());
- // 4. Create a new data block.
- currentDataBlock = HFileDataBlock.createDataBlockToWrite(context,
currentOffset);
+ // 4. Create a new data block whose previous-block offset points at the
block just flushed.
+ currentDataBlock = HFileDataBlock.createDataBlockToWrite(context,
lastDataBlockOffset);
}
// NOTE that: reader assumes that every meta info piece
@@ -200,7 +200,7 @@ public class HFileWriterImpl implements HFileWriter {
ByteBuffer trailer = ByteBuffer.allocate(TRAILER_SIZE);
trailer.limit(TRAILER_SIZE);
trailer.put(TRAILER.getMagic());
-
trailer.put(getVariableLengthEncodedBytes(trailerProto.getSerializedSize()));
+ trailer.put(getProtobufVarIntBytes(trailerProto.getSerializedSize()));
trailer.put(trailerProto.toByteArray());
// Force trailer to have fixed length.
trailer.position(TRAILER_SIZE - 1);
diff --git a/hudi-io/src/main/java/org/apache/hudi/io/util/IOUtils.java
b/hudi-io/src/main/java/org/apache/hudi/io/util/IOUtils.java
index 68482c1a4740..087d0251f79e 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/util/IOUtils.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/util/IOUtils.java
@@ -148,42 +148,6 @@ public class IOUtils {
return value < -120 || (value >= -112 && value < 0);
}
- /**
- * Encodes an integer using Hadoop-compatible variable-length encoding
- * (WritableUtils VarInt format) and returns the encoded bytes.
- *
- * <p>For values between -112 and 127 (inclusive), a single byte is used.
- * For other values, the first byte indicates the number of following bytes
- * and the sign, followed by the value in big-endian order.
- *
- * @param value the integer value to encode.
- * @return the encoded byte array.
- */
- public static byte[] writeVarInt(int value) {
- if (value >= -112 && value <= 127) {
- return new byte[] {(byte) value};
- }
- long longValue = value;
- int len = -112;
- if (longValue < 0) {
- longValue ^= -1L;
- len = -120;
- }
- long tmp = longValue;
- while (tmp != 0) {
- tmp >>= 8;
- len--;
- }
- int numBytes = (len < -120) ? -(len + 120) : -(len + 112);
- byte[] result = new byte[1 + numBytes];
- result[0] = (byte) len;
- for (int idx = 0; idx < numBytes; idx++) {
- int shiftBits = (numBytes - idx - 1) * 8;
- result[1 + idx] = (byte) ((longValue >> shiftBits) & 0xFF);
- }
- return result;
- }
-
/**
* @param bytes input byte array.
* @param offset offset to start reading.
diff --git
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileReadCompatibility.java
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileReadCompatibility.java
index c190a62fded3..b4130a1afda2 100644
---
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileReadCompatibility.java
+++
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileReadCompatibility.java
@@ -298,6 +298,33 @@ class TestHFileReadCompatibility {
}
}
+ /**
+ * The data block's previous-block-offset header must be correct: the HBase
reader uses it in
+ * {@link HFileScanner#seekBefore} to step back to the prior block when the
target lands on a
+ * block's first key. With a wrong offset (e.g. the block's own offset) the
HBase reader re-reads
+ * the same block and throws {@code IllegalStateException} at every block
boundary. On a
+ * native-written multi-block file, {@code seekBefore(key(i))} must return
{@code key(i - 1)} for
+ * every record. The native reader is forward-only and never reads this
field.
+ */
+ @Test
+ void hbaseReaderSeekBeforeWorksOnNativeMultiBlockFile() throws IOException {
+ byte[] data = writeMultiBlockHudiHFile(MULTI_BLOCK_RECORDS,
SMALL_BLOCK_SIZE, CompressionCodec.NONE);
+ try (HFile.Reader hbaseReader = createHBaseHFileReader(data)) {
+ assertTrue(hbaseReader.getTrailer().getDataIndexCount() > 1,
+ "precondition: the file must have multiple data blocks for
seekBefore to cross boundaries");
+ HFileScanner scanner = hbaseReader.getScanner(true, true);
+ for (int i = 1; i < MULTI_BLOCK_RECORDS; i++) {
+ KeyValue probe = new KeyValue(key(i).getBytes(StandardCharsets.UTF_8),
+ new byte[0], new byte[0], HConstants.LATEST_TIMESTAMP, new
byte[0]);
+ assertTrue(scanner.seekBefore(probe), "seekBefore should find a key
before " + key(i));
+ Cell c = scanner.getCell();
+ assertEquals(key(i - 1),
+ Bytes.toString(c.getRowArray(), c.getRowOffset(),
c.getRowLength()),
+ "seekBefore(" + key(i) + ") must land on the immediately preceding
key");
+ }
+ }
+ }
+
static boolean isPrefix(byte[] prefix, byte[] array) {
if (prefix.length > array.length) {
return false; // can't be prefix if longer
diff --git
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileWriter.java
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileWriter.java
index e1995684c88d..23e8d7007961 100644
--- a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileWriter.java
+++ b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileWriter.java
@@ -22,8 +22,10 @@ package org.apache.hudi.io.hfile;
import org.apache.hudi.io.ByteArraySeekableDataInputStream;
import org.apache.hudi.io.ByteBufferBackedInputStream;
import org.apache.hudi.io.SeekableDataInputStream;
+import org.apache.hudi.io.hfile.protobuf.generated.HFileProtos;
+import org.apache.hudi.io.util.IOUtils;
-import lombok.extern.slf4j.Slf4j;
+import com.google.protobuf.CodedOutputStream;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.hbase.CellComparatorImpl;
@@ -32,9 +34,15 @@ import org.apache.hadoop.hbase.io.compress.Compression;
import org.apache.hadoop.hbase.io.hfile.CacheConfig;
import org.apache.hadoop.hbase.io.hfile.HFile;
import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
+import org.apache.hadoop.io.DataOutputBuffer;
+import org.apache.hadoop.io.WritableUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -44,23 +52,32 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
+import static org.apache.hudi.io.hfile.HFileBlock.HFILEBLOCK_HEADER_SIZE;
import static org.apache.hudi.io.hfile.HFileBlockType.DATA;
+import static org.apache.hudi.io.hfile.HFileBlockType.FILE_INFO;
+import static org.apache.hudi.io.hfile.HFileBlockType.META;
+import static org.apache.hudi.io.hfile.HFileBlockType.ROOT_INDEX;
import static org.apache.hudi.io.hfile.HFileBlockType.TRAILER;
import static org.apache.hudi.io.hfile.HFileInfo.AVG_KEY_LEN;
import static org.apache.hudi.io.hfile.HFileInfo.AVG_VALUE_LEN;
import static org.apache.hudi.io.hfile.HFileInfo.KEY_VALUE_VERSION;
import static org.apache.hudi.io.hfile.HFileInfo.LAST_KEY;
import static org.apache.hudi.io.hfile.HFileInfo.MAX_MVCC_TS_KEY;
+import static org.apache.hudi.io.hfile.HFileTrailer.TRAILER_SIZE;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-@Slf4j
class TestHFileWriter {
+ private static final Logger LOG =
LoggerFactory.getLogger(TestHFileWriter.class);
private static final String TEST_FILE = "test.hfile";
private static final HFileContext CONTEXT = HFileContext.builder().build();
// Golden bytes (NONE compression, fixed input) that lock the on-disk
encoding of the data block
@@ -73,6 +90,20 @@ class TestHFileWriter {
private static final String GOLDEN_ROOT_INDEX_BLOCK_HEX =
"494458524f4f5432000000210000001dffffffffffffffff00000040000000003e000000000000000000000082100004"
+ "6b657931007fffffffffffffff0400000000";
+ // Golden for a single block holding keys of different lengths whose first
key (116 chars) lands in
+ // the multi-byte vint range (keyLength 128). Pinned for both the native and
HBase writers.
+ private static final String GOLDEN_VARLEN_DATA_REGION_HEX =
+
"44415441424c4b2a000000d1000000cdffffffffffffffff0000004000000000ee000000800000000200746161616161"
+ +
"616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"
+ +
"616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"
+ +
"616161616161616161616161616161007fffffffffffffff04763000000000140000000200086262626262626262007f"
+ +
"ffffffffffffff047631000000001800000002000c636363636363636363636363007fffffffffffffff047632000000"
+ + "0000";
+ private static final String GOLDEN_VARLEN_ROOT_INDEX_BLOCK_HEX =
+
"494458524f4f5432000000920000008effffffffffffffff0000004000000000af0000000000000000000000f28f8000"
+ +
"746161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"
+ +
"616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"
+ +
"616161616161616161616161616161616161616161007fffffffffffffff0400000000";
@AfterEach
public void tearDown() throws IOException {
@@ -89,7 +120,7 @@ class TestHFileWriter {
validateHFileStructure();
// 4. Validate consistency with HFileReader.
validateConsistencyWithHFileReader();
- log.info("All validations passed!");
+ LOG.info("All validations passed!");
}
@Test
@@ -200,84 +231,124 @@ class TestHFileWriter {
);
reader.next();
}
+
+ // Each data block's previous-block-offset header (8 bytes at offset 16)
must chain back to
+ // the prior block, and -1 for the first, so an HBase reader's
seekBefore can step back across
+ // blocks. The writer once set it to the block's own offset; this guards
that regression.
+ byte[] file = Files.readAllBytes(Paths.get(testFile));
+ List<IndexEntry> dataIndex = parseIndexBlock(
+ file, (int) trailer.getLoadOnOpenDataOffset(),
trailer.getDataIndexCount());
+ assertTrue(dataIndex.size() > 1, "test setup: expected multiple data
blocks");
+ for (int i = 0; i < dataIndex.size(); i++) {
+ long prevOffset = readLongBE(file, (int) dataIndex.get(i).offset + 16);
+ long expected = i == 0 ? -1L : dataIndex.get(i - 1).offset;
+ assertEquals(expected, prevOffset, "data block " + i + "
previous-block offset");
+ }
}
}
+ /**
+ * Format lock for the native writer's on-disk bytes. Single-block inputs
are checked against the
+ * HBase writer (proving the writers agree on the encoding): short keys
(single-byte index
+ * keyLength) and the 116-char boundary (keyLength 128, where Protobuf and
Hadoop VInt diverge) are
+ * also pinned to a golden; 500- and 2000-char first keys (3-byte keyLength)
are checked against
+ * HBase without a golden. The golden cases also validate the trailer and
file info bytes. A single
+ * block is deliberate for cross-writer checks: across blocks HBase stores
shortened index separator
+ * keys, so only a single full-key entry is byte-comparable. A multi-block
native file then covers
+ * the meta block and the rest of the section byte layout and round-trips.
+ */
@Test
- void testLongKeys() throws IOException {
- // Test that HFile blocks with long keys (>= 126 chars) can be written and
read correctly.
- // This verifies the fix for the varint encoding mismatch in the root
index block.
- HFileContext context = new HFileContext.Builder().blockSize(100).build();
- String testFile = TEST_FILE;
- int numRecords = 10;
- // Generate keys longer than 126 characters to trigger multi-byte Hadoop
VarInt encoding
- // in the root index block. The varint encodes (key_content_length + 2),
so content >= 126
- // produces a value >= 128 which requires 2+ bytes in Hadoop VarInt format.
- char[] chars = new char[200];
- Arrays.fill(chars, 'a');
- String longPrefix = new String(chars);
- try (DataOutputStream outputStream =
- new DataOutputStream(Files.newOutputStream(Paths.get(testFile)));
- HFileWriter writer = new HFileWriterImpl(context, outputStream)) {
- for (int i = 0; i < numRecords; i++) {
- String key = longPrefix + String.format("%04d", i);
- writer.append(key, String.format("value%04d", i).getBytes());
- }
- }
+ void writerBlockBytesAreStableFormatLock() throws Exception {
+ byte[][] vals = {bytes("v0"), bytes("v1"), bytes("v2")};
+ assertSingleBlockBytesMatchGoldenAndHBase(
+ new String[] {"key1", "key2", "key3"},
+ new byte[][] {bytes("value1"), bytes("value2"), bytes("value3")},
+ GOLDEN_DATA_REGION_HEX, GOLDEN_ROOT_INDEX_BLOCK_HEX);
+ assertSingleBlockBytesMatchGoldenAndHBase(
+ new String[] {rep('a', 116), rep('b', 8), rep('c', 12)}, vals,
+ GOLDEN_VARLEN_DATA_REGION_HEX, GOLDEN_VARLEN_ROOT_INDEX_BLOCK_HEX);
+ // Larger first keys (3-byte index keyLength) checked against the HBase
writer without a golden,
+ // which for multi-KB key bytes would add no signal.
+ assertSingleBlockBytesMatchHBase(new String[] {rep('a', 500), rep('b', 8),
rep('c', 12)}, vals, null, null);
+ assertSingleBlockBytesMatchHBase(new String[] {rep('a', 2000), rep('b',
8), rep('c', 12)}, vals, null, null);
+ assertAllSectionsValidOnMultiBlockFile();
+ }
- // Validate that all records can be read back correctly.
- try (FileChannel channel = FileChannel.open(Paths.get(testFile),
StandardOpenOption.READ)) {
- ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0,
channel.size());
- SeekableDataInputStream inputStream =
- new ByteArraySeekableDataInputStream(new
ByteBufferBackedInputStream(buf));
- HFileReaderImpl reader = new HFileReaderImpl(inputStream,
channel.size());
- reader.initializeMetadata();
- assertEquals(numRecords, reader.getNumKeyValueEntries());
- reader.seekTo();
- for (int i = 0; i < numRecords; i++) {
- KeyValue kv = reader.getKeyValue().get();
- String expectedKey = longPrefix + String.format("%04d", i);
- assertEquals(expectedKey, kv.getKey().getContentInString());
- assertArrayEquals(
- String.format("value%04d", i).getBytes(),
- Arrays.copyOfRange(
- kv.getBytes(),
- kv.getValueOffset(),
- kv.getValueOffset() + kv.getValueLength())
- );
- reader.next();
- }
+ @Test
+ void getVarIntBytesRoundTripsThroughReadVarLong() {
+ // HFileIndexBlock.getVarIntBytes output must decode back via the reader's
IOUtils.readVarLong.
+ int[] testValues = {0, 1, 127, 128, 146, 200, 255, 256, 300, 1000, 32080,
65535, 100000,
+ 2034958, 632492350, Integer.MAX_VALUE};
+ for (int value : testValues) {
+ byte[] encoded = HFileIndexBlock.getVarIntBytes(value);
+ int size = IOUtils.decodeVarLongSizeOnDisk(encoded, 0);
+ assertEquals(encoded.length, size, "Size mismatch for value " + value);
+ long decoded = IOUtils.readVarLong(encoded, 0, size);
+ assertEquals(value, decoded, "Round-trip mismatch for value " + value);
}
}
+ @Test
+ void getVarIntBytesMatchesKnownHadoopVectors() {
+ // Cross-check HFileIndexBlock.getVarIntBytes against known Hadoop
WritableUtils VInt encodings.
+ assertEquals(1, HFileIndexBlock.getVarIntBytes(0).length);
+ assertEquals(0, HFileIndexBlock.getVarIntBytes(0)[0]);
+ assertEquals(1, HFileIndexBlock.getVarIntBytes(98).length);
+ assertEquals(98, HFileIndexBlock.getVarIntBytes(98)[0]);
+
+ // Value 208 requires 2 bytes.
+ byte[] enc208 = HFileIndexBlock.getVarIntBytes(208);
+ assertEquals(2, enc208.length);
+ assertEquals(208, IOUtils.readVarLong(enc208, 0));
+
+ // Value 32080 requires 3 bytes.
+ byte[] enc32080 = HFileIndexBlock.getVarIntBytes(32080);
+ assertEquals(3, enc32080.length);
+ assertEquals(32080, IOUtils.readVarLong(enc32080, 0));
+ }
+
/**
- * Format lock: with NONE compression and a fixed input the data block and
root block-index block
- * are deterministic, so their raw bytes are asserted against a golden. The
same records written
- * by the HBase HFile writer (NONE compression, NULL checksum, latest
timestamp, Put type) must
- * produce the same two block byte regions, proving the native and HBase
writers agree on the
- * on-disk encoding. Any change to the encoding (dropping the KeyValue
suffix, ts/type, or
- * framing) fails here. Neither block holds the file-creation timestamp, so
the bytes are stable.
+ * Pins a single-block file's data block and root index block bytes to a
golden the HBase writer
+ * must also reproduce, and validates the remaining sections of the native
file (trailer, file
+ * info, and the empty meta index). The file has no meta block: a meta block
would shift the data
+ * region and HBase serializes it differently, breaking the byte-parity (the
meta block is covered
+ * by {@link #assertAllSectionsValidOnMultiBlockFile()}).
*/
- @Test
- void writerBlockBytesAreStableFormatLock() throws Exception {
- writeTestFile();
- String[] nativeBlocks =
dataAndRootIndexBlockHex(Files.readAllBytes(Paths.get(TEST_FILE)));
- // Logged so the golden can be regenerated intentionally.
- log.info("GOLDEN_DATA_REGION_HEX={}", nativeBlocks[0]);
- log.info("GOLDEN_ROOT_INDEX_BLOCK_HEX={}", nativeBlocks[1]);
- assertEquals(GOLDEN_DATA_REGION_HEX, nativeBlocks[0],
- "native data block bytes changed (storage-format change); review HBase
compatibility");
- assertEquals(GOLDEN_ROOT_INDEX_BLOCK_HEX, nativeBlocks[1],
- "native root block-index bytes changed (storage-format change); review
HBase compatibility");
-
- // The HBase writer, given the same records, must produce the same two
block byte regions.
- String[] hbaseBlocks = dataAndRootIndexBlockHex(writeFixedHBaseFile());
- log.info("HBASE_DATA_REGION_HEX={}", hbaseBlocks[0]);
- log.info("HBASE_ROOT_INDEX_BLOCK_HEX={}", hbaseBlocks[1]);
- assertEquals(GOLDEN_DATA_REGION_HEX, hbaseBlocks[0],
- "HBase writer data block bytes differ from the native writer");
- assertEquals(GOLDEN_ROOT_INDEX_BLOCK_HEX, hbaseBlocks[1],
- "HBase writer root block-index bytes differ from the native writer");
+ private static void assertSingleBlockBytesMatchGoldenAndHBase(
+ String[] keys, byte[][] values, String dataGolden, String rootGolden)
throws Exception {
+ assertSingleBlockBytesMatchHBase(keys, values, dataGolden, rootGolden);
+ byte[] nativeFile = Files.readAllBytes(Paths.get(TEST_FILE));
+ HFileProtos.TrailerProto trailer = validateTrailerSection(nativeFile,
keys.length, 0);
+ int loadOnOpen = (int) trailer.getLoadOnOpenDataOffset();
+ int metaIndexOffset = loadOnOpen + HFILEBLOCK_HEADER_SIZE +
readIntBE(nativeFile, loadOnOpen + 8);
+ assertBytesEqual("empty meta index magic", ROOT_INDEX.getMagic(),
+ Arrays.copyOfRange(nativeFile, metaIndexOffset, metaIndexOffset + 8));
+ validateFileInfoSection(
+ nativeFile, metaIndexOffset, (int) trailer.getFileInfoOffset(),
keys[keys.length - 1]);
+ }
+
+ /**
+ * Asserts the native and HBase writers produce identical single-block data
and root index bytes
+ * (optionally also matching a pinned golden), and the root-index keyLength
is Hadoop VInt. Used
+ * without a golden for large keys, where a hardcoded golden of multi-KB key
bytes adds no signal.
+ */
+ private static void assertSingleBlockBytesMatchHBase(
+ String[] keys, byte[][] values, String dataGolden, String rootGolden)
throws Exception {
+ writeNativeFile(TEST_FILE, HFileContext.builder().build(), keys, values,
null, null);
+ byte[] nativeFile = Files.readAllBytes(Paths.get(TEST_FILE));
+ String[] nativeBlocks = dataAndRootIndexBlockHex(nativeFile);
+ String[] hbaseBlocks = dataAndRootIndexBlockHex(writeHBaseFile(keys,
values));
+ assertEquals(hbaseBlocks[0], nativeBlocks[0], "native vs HBase data block
bytes");
+ assertEquals(hbaseBlocks[1], nativeBlocks[1], "native vs HBase root index
bytes");
+ if (dataGolden != null) {
+ LOG.info("DATA_REGION_HEX={}", nativeBlocks[0]);
+ LOG.info("ROOT_INDEX_BLOCK_HEX={}", nativeBlocks[1]);
+ assertEquals(dataGolden, nativeBlocks[0], "data block bytes changed
(storage-format change)");
+ assertEquals(rootGolden, nativeBlocks[1], "root index bytes changed
(storage-format change)");
+ }
+ IndexEntry first =
+ parseIndexBlock(nativeFile, indexOf(nativeFile,
ROOT_INDEX.getMagic()), 1).get(0);
+ assertVarIntIsHadoopNotProtobuf("root index keyLength", first);
}
/** Returns {@code [dataRegionHex, rootIndexBlockHex]} for an HFile's raw
bytes. */
@@ -292,8 +363,8 @@ class TestHFileWriter {
};
}
- /** Writes key1/key2/key3 with the HBase HFile writer, matching the native
writer's settings. */
- private static byte[] writeFixedHBaseFile() throws IOException {
+ /** Writes the given records with the HBase HFile writer, matching the
native writer's settings. */
+ private static byte[] writeHBaseFile(String[] keys, byte[][] values) throws
IOException {
Configuration conf = new Configuration();
FileSystem fs = FileSystem.getLocal(conf);
org.apache.hadoop.fs.Path path =
@@ -307,15 +378,270 @@ class TestHFileWriter {
.build();
try (HFile.Writer writer = HFile.getWriterFactory(conf, new
CacheConfig(conf))
.withPath(fs, path).withFileContext(context).create()) {
- for (int i = 1; i <= 3; i++) {
+ for (int i = 0; i < keys.length; i++) {
writer.append(new org.apache.hadoop.hbase.KeyValue(
- ("key" + i).getBytes(StandardCharsets.UTF_8), new byte[0], new
byte[0],
- HConstants.LATEST_TIMESTAMP, ("value" +
i).getBytes(StandardCharsets.UTF_8)));
+ keys[i].getBytes(StandardCharsets.UTF_8), new byte[0], new byte[0],
+ HConstants.LATEST_TIMESTAMP, values[i]));
}
}
return Files.readAllBytes(Paths.get(path.toString()));
}
+ /**
+ * Validates the raw bytes of every HFile section on a multi-block native
file with one meta block:
+ * scanned data blocks, the non-scanned meta block, the load-on-open root
data index / meta index /
+ * file info, and the trailer, then reads every record back. Index
keyLengths are asserted Hadoop
+ * {@code WritableUtils} VInt; the file info and trailer length prefixes
protobuf-delimited. Each
+ * section is checked in its own helper so callers stay under the per-test
assertion limit.
+ */
+ private static void assertAllSectionsValidOnMultiBlockFile() throws
IOException {
+ int numRecords = 8;
+ String[] keys = new String[numRecords];
+ byte[][] values = new byte[numRecords][];
+ for (int i = 0; i < numRecords; i++) {
+ keys[i] = makeKey(200, i);
+ values[i] = bytes("v" + i);
+ }
+ String metaKey = makeKey(200, 9999);
+ byte[] metaValue = bytes("bloom-filter-payload-bytes");
+ // Small block size forces one record per block, so the root index has
multiple entries.
+ writeNativeFile(
+ TEST_FILE, new HFileContext.Builder().blockSize(100).build(), keys,
values, metaKey, metaValue);
+ byte[] file = Files.readAllBytes(Paths.get(TEST_FILE));
+
+ HFileProtos.TrailerProto trailer = validateTrailerSection(file,
numRecords, 1);
+ int loadOnOpen = (int) trailer.getLoadOnOpenDataOffset();
+ validateRootDataIndexSection(file, loadOnOpen,
trailer.getDataIndexCount(), keys[0]);
+ int metaIndexOffset = loadOnOpen + HFILEBLOCK_HEADER_SIZE +
readIntBE(file, loadOnOpen + 8);
+ validateMetaIndexAndBlock(file, metaIndexOffset, metaKey, metaValue);
+ validateFileInfoSection(file, metaIndexOffset, (int)
trailer.getFileInfoOffset(), keys[numRecords - 1]);
+ assertAllRecordsReadBack(numRecords, keys);
+ }
+
+ /** Validates the trailer magic, protobuf framing, fields, and HFile
version; returns the proto. */
+ private static HFileProtos.TrailerProto validateTrailerSection(
+ byte[] file, int numRecords, int metaIndexCount) throws IOException {
+ int trailerStart = file.length - TRAILER_SIZE;
+ assertBytesEqual("trailer magic",
+ TRAILER.getMagic(), Arrays.copyOfRange(file, trailerStart,
trailerStart + 8));
+ HFileProtos.TrailerProto trailer =
HFileProtos.TrailerProto.parseDelimitedFrom(
+ new ByteArrayInputStream(file, trailerStart + 8, TRAILER_SIZE - 8));
+ assertProtobufDelimitedFraming("trailer", file, trailerStart + 8,
trailer.getSerializedSize());
+ assertEquals(numRecords, trailer.getEntryCount());
+ assertEquals(metaIndexCount, trailer.getMetaIndexCount());
+ assertEquals(1, trailer.getNumDataIndexLevels());
+ assertEquals(2, trailer.getCompressionCodec(), "compression codec must be
NONE (2)");
+ assertEquals("org.apache.hudi.io.storage.HoodieHBaseKVComparator",
+ trailer.getComparatorClassName());
+ assertEquals(0, trailer.getFirstDataBlockOffset(), "scanned section starts
at offset 0");
+ // HFile version: last 4 bytes hold the major version (3).
+ assertEquals(3, readIntBE(file, file.length - 4), "HFile major version");
+ return trailer;
+ }
+
+ /** Validates the scanned-section start and every root data index entry. */
+ private static void validateRootDataIndexSection(
+ byte[] file, int loadOnOpen, int dataIndexCount, String firstKey) throws
IOException {
+ assertBytesEqual("first data block magic", DATA.getMagic(),
Arrays.copyOfRange(file, 0, 8));
+ assertBytesEqual("root index magic",
+ ROOT_INDEX.getMagic(), Arrays.copyOfRange(file, loadOnOpen, loadOnOpen
+ 8));
+ List<IndexEntry> dataIndex = parseIndexBlock(file, loadOnOpen,
dataIndexCount);
+ assertEquals(dataIndexCount, dataIndex.size(), "root index entry count");
+ for (IndexEntry e : dataIndex) {
+ // keyLength must be Hadoop WritableUtils VInt (never protobuf varint),
and each entry must
+ // point at a real DATA block whose on-disk size matches the entry's
size.
+ assertVarIntIsHadoopNotProtobuf("data index keyLength", e);
+ assertBytesEqual("data block magic at index offset",
+ DATA.getMagic(), Arrays.copyOfRange(file, (int) e.offset, (int)
e.offset + 8));
+ assertEquals(HFILEBLOCK_HEADER_SIZE + readIntBE(file, (int) e.offset +
8), e.size,
+ "data index entry size must equal the on-disk size of the referenced
block");
+ }
+ assertEquals(firstKey, keyContentString(dataIndex.get(0).key), "first
index entry key content");
+ }
+
+ /** Validates the meta index entry and the meta block it points at. */
+ private static void validateMetaIndexAndBlock(
+ byte[] file, int metaIndexOffset, String metaKey, byte[] metaValue)
throws IOException {
+ assertBytesEqual("meta index magic",
+ ROOT_INDEX.getMagic(), Arrays.copyOfRange(file, metaIndexOffset,
metaIndexOffset + 8));
+ IndexEntry metaEntry = parseIndexBlock(file, metaIndexOffset, 1).get(0);
+ assertVarIntIsHadoopNotProtobuf("meta index keyLength", metaEntry);
+ assertBytesEqual("meta index key bytes",
metaKey.getBytes(StandardCharsets.UTF_8), metaEntry.key);
+ assertBytesEqual("meta block magic",
+ META.getMagic(), Arrays.copyOfRange(file, (int) metaEntry.offset,
(int) metaEntry.offset + 8));
+ int metaPayload = (int) metaEntry.offset + HFILEBLOCK_HEADER_SIZE;
+ assertBytesEqual("meta block payload == meta value",
+ metaValue, Arrays.copyOfRange(file, metaPayload, metaPayload +
metaValue.length));
+ }
+
+ /** Validates the file info block offset, magic, PBUF framing, and decoded
entries. */
+ private static void validateFileInfoSection(
+ byte[] file, int metaIndexOffset, int fileInfoOffset, String lastKey)
throws IOException {
+ int metaIndexBlockSize = HFILEBLOCK_HEADER_SIZE + readIntBE(file,
metaIndexOffset + 8);
+ assertEquals(metaIndexOffset + metaIndexBlockSize, fileInfoOffset,
+ "trailer fileInfoOffset must point right after the meta index block");
+ assertBytesEqual("file info magic",
+ FILE_INFO.getMagic(), Arrays.copyOfRange(file, fileInfoOffset,
fileInfoOffset + 8));
+ int pbufStart = fileInfoOffset + HFILEBLOCK_HEADER_SIZE;
+ assertBytesEqual("file info PBUF magic",
+ "PBUF".getBytes(StandardCharsets.UTF_8), Arrays.copyOfRange(file,
pbufStart, pbufStart + 4));
+ HFileProtos.InfoProto info = HFileProtos.InfoProto.parseDelimitedFrom(
+ new ByteArrayInputStream(file, pbufStart + 4, file.length - (pbufStart
+ 4)));
+ // File info uses protobuf-delimited framing (correct here, unlike the
index keyLength fields).
+ assertProtobufDelimitedFraming("file info", file, pbufStart + 4,
info.getSerializedSize());
+ Map<String, byte[]> infoMap = new LinkedHashMap<>();
+ for (HFileProtos.BytesBytesPair pair : info.getMapEntryList()) {
+ infoMap.put(new String(pair.getFirst().toByteArray(),
StandardCharsets.UTF_8),
+ pair.getSecond().toByteArray());
+ }
+ for (UTF8StringKey required : new UTF8StringKey[] {
+ LAST_KEY, KEY_VALUE_VERSION, MAX_MVCC_TS_KEY, AVG_KEY_LEN,
AVG_VALUE_LEN}) {
+ assertTrue(infoMap.containsKey(utf8(required)), "file info must contain
" + utf8(required));
+ }
+ assertEquals(lastKey, keyContentString(infoMap.get(utf8(LAST_KEY))),
+ "LASTKEY content must be the last appended key");
+ }
+
+ /** Reads every record back through the native reader. */
+ private static void assertAllRecordsReadBack(int numRecords, String[] keys)
throws IOException {
+ try (FileChannel channel = FileChannel.open(Paths.get(TEST_FILE),
StandardOpenOption.READ)) {
+ ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0,
channel.size());
+ HFileReaderImpl reader = new HFileReaderImpl(
+ new ByteArraySeekableDataInputStream(new
ByteBufferBackedInputStream(buf)), channel.size());
+ reader.initializeMetadata();
+ assertEquals(numRecords, reader.getNumKeyValueEntries());
+ reader.seekTo();
+ for (int i = 0; i < numRecords; i++) {
+ assertEquals(keys[i],
reader.getKeyValue().get().getKey().getContentInString());
+ reader.next();
+ }
+ }
+ }
+
+ /** Writes the given records (and an optional single meta block) with the
native HFile writer. */
+ private static void writeNativeFile(String testFile, HFileContext context,
String[] keys,
+ byte[][] values, String metaKey, byte[]
metaValue)
+ throws IOException {
+ try (DataOutputStream outputStream =
+ new DataOutputStream(Files.newOutputStream(Paths.get(testFile)));
+ HFileWriter writer = new HFileWriterImpl(context, outputStream)) {
+ for (int i = 0; i < keys.length; i++) {
+ writer.append(keys[i], values[i]);
+ }
+ if (metaKey != null) {
+ writer.appendMetaInfo(metaKey, metaValue);
+ }
+ }
+ }
+
+ /** A parsed block index entry: block offset, on-disk size, raw keyLength
vint bytes, and key. */
+ private static final class IndexEntry {
+ long offset;
+ int size;
+ byte[] keyLengthVarIntBytes;
+ int keyLen;
+ byte[] key;
+ }
+
+ /** Parses {@code numEntries} block index entries starting at the given
index block offset. */
+ private static List<IndexEntry> parseIndexBlock(byte[] file, int blockStart,
int numEntries) {
+ List<IndexEntry> entries = new ArrayList<>();
+ int p = blockStart + HFILEBLOCK_HEADER_SIZE;
+ for (int i = 0; i < numEntries; i++) {
+ IndexEntry e = new IndexEntry();
+ e.offset = readLongBE(file, p);
+ e.size = readIntBE(file, p + 8);
+ int varIntSize = IOUtils.decodeVarLongSizeOnDisk(file, p + 12);
+ e.keyLen = (int) IOUtils.readVarLong(file, p + 12, varIntSize);
+ e.keyLengthVarIntBytes = Arrays.copyOfRange(file, p + 12, p + 12 +
varIntSize);
+ e.key = Arrays.copyOfRange(file, p + 12 + varIntSize, p + 12 +
varIntSize + e.keyLen);
+ entries.add(e);
+ p += 12 + varIntSize + e.keyLen;
+ }
+ return entries;
+ }
+
+ /**
+ * Asserts that an index entry's keyLength bytes equal Hadoop {@code
WritableUtils} VInt (the
+ * encoding the reader and HBase decode), and for values >= 128, that they
are NOT protobuf
+ * varint (the pre-fix encoding that produced negative key lengths in
HBase's reader).
+ */
+ private static void assertVarIntIsHadoopNotProtobuf(String message,
IndexEntry e)
+ throws IOException {
+ assertBytesEqual(message + " must be Hadoop WritableUtils VInt",
+ hadoopWritableUtilsVInt(e.keyLen), e.keyLengthVarIntBytes);
+ if (e.keyLen >= 128) {
+ assertNotEquals(hex(protobufVarInt(e.keyLen)),
hex(e.keyLengthVarIntBytes),
+ message + " must NOT be protobuf varint for values >= 128 (the
original bug)");
+ }
+ }
+
+ /** Asserts the on-disk length prefix at {@code offset} is a protobuf varint
of {@code size}. */
+ private static void assertProtobufDelimitedFraming(String section, byte[]
file, int offset,
+ int serializedSize)
throws IOException {
+ byte[] expectedPrefix = protobufVarInt(serializedSize);
+ assertBytesEqual(section + " must use protobuf-delimited framing",
+ expectedPrefix, Arrays.copyOfRange(file, offset, offset +
expectedPrefix.length));
+ }
+
+ /** Reference Hadoop {@code WritableUtils} VInt encoder (the encoding
HBase's reader expects). */
+ private static byte[] hadoopWritableUtilsVInt(long value) throws IOException
{
+ DataOutputBuffer out = new DataOutputBuffer();
+ WritableUtils.writeVInt(out, (int) value);
+ return Arrays.copyOf(out.getData(), out.getLength());
+ }
+
+ /** Reference protobuf varint encoder (the pre-fix, HBase-incompatible
encoding for the index). */
+ private static byte[] protobufVarInt(int value) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeUInt32NoTag(value);
+ cos.flush();
+ return baos.toByteArray();
+ }
+
+ /** Extracts the row (key content) bytes from a serialized HBase KeyValue
key, as a String. */
+ private static String keyContentString(byte[] keyValueKey) {
+ int rowLength = ((keyValueKey[0] & 0xff) << 8) | (keyValueKey[1] & 0xff);
+ return new String(keyValueKey, 2, rowLength, StandardCharsets.UTF_8);
+ }
+
+ /** Builds a length-{@code len} key that sorts by {@code i} and is unique. */
+ private static String makeKey(int len, int i) {
+ String suffix = String.format("%04d", i);
+ StringBuilder sb = new StringBuilder(len);
+ for (int k = 0; k < len - suffix.length(); k++) {
+ sb.append('a');
+ }
+ return sb.append(suffix).toString();
+ }
+
+ private static String utf8(UTF8StringKey key) {
+ return new String(key.getBytes(), StandardCharsets.UTF_8);
+ }
+
+ private static void assertBytesEqual(String message, byte[] expected, byte[]
actual) {
+ assertEquals(hex(expected), hex(actual), message);
+ }
+
+ private static String rep(char c, int n) {
+ char[] a = new char[n];
+ Arrays.fill(a, c);
+ return new String(a);
+ }
+
+ private static byte[] bytes(String s) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static long readLongBE(byte[] b, int off) {
+ long v = 0;
+ for (int i = 0; i < 8; i++) {
+ v = (v << 8) | (b[off + i] & 0xffL);
+ }
+ return v;
+ }
+
private static void writeTestFile() throws Exception {
try (
DataOutputStream outputStream =
diff --git a/hudi-io/src/test/java/org/apache/hudi/io/util/TestIOUtils.java
b/hudi-io/src/test/java/org/apache/hudi/io/util/TestIOUtils.java
index a100446428e1..bc20d47a860b 100644
--- a/hudi-io/src/test/java/org/apache/hudi/io/util/TestIOUtils.java
+++ b/hudi-io/src/test/java/org/apache/hudi/io/util/TestIOUtils.java
@@ -92,39 +92,6 @@ public class TestIOUtils {
assertEquals(expectedNumber < 0, IOUtils.isNegativeVarLong(bytes[0]));
}
- @Test
- public void testWriteVarIntRoundTrip() {
- // Verify that writeVarInt produces bytes that readVarLong can decode
correctly
- int[] testValues = {0, 1, 127, 128, 146, 200, 255, 256, 300, 1000, 32080,
65535, 100000,
- 2034958, 632492350, Integer.MAX_VALUE};
- for (int value : testValues) {
- byte[] encoded = IOUtils.writeVarInt(value);
- int size = IOUtils.decodeVarLongSizeOnDisk(encoded, 0);
- assertEquals(encoded.length, size, "Size mismatch for value " + value);
- long decoded = IOUtils.readVarLong(encoded, 0, size);
- assertEquals(value, decoded, "Round-trip mismatch for value " + value);
- }
- }
-
- @Test
- public void testWriteVarIntMatchesExistingTestVectors() {
- // Cross-check writeVarInt against known Hadoop VarLong encoding from the
existing test data
- assertEquals(1, IOUtils.writeVarInt(0).length);
- assertEquals(0, IOUtils.writeVarInt(0)[0]);
- assertEquals(1, IOUtils.writeVarInt(98).length);
- assertEquals(98, IOUtils.writeVarInt(98)[0]);
-
- // Value 208 requires 2 bytes
- byte[] enc208 = IOUtils.writeVarInt(208);
- assertEquals(2, enc208.length);
- assertEquals(208, IOUtils.readVarLong(enc208, 0));
-
- // Value 32080 requires 3 bytes
- byte[] enc32080 = IOUtils.writeVarInt(32080);
- assertEquals(3, enc32080.length);
- assertEquals(32080, IOUtils.readVarLong(enc32080, 0));
- }
-
@Test
public void testByteArrayCompareTo() {
byte[] bytes1 = new byte[] {(byte) 0x9b, 0, 0x18, 0x65, 0x2e, (byte) 0xf3};