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 39880539ec56 fix(storage-format): emit HBase-readable block-index keys
in the native HFile writer (#19071)
39880539ec56 is described below
commit 39880539ec56cb3166abb8f1f10851d341abccdb
Author: Y Ethan Guo <[email protected]>
AuthorDate: Tue Aug 11 03:04:29 2026 -0700
fix(storage-format): emit HBase-readable block-index keys in the native
HFile writer (#19071)
---
hudi-io/hfile_format.md | 10 +-
.../java/org/apache/hudi/io/hfile/HFileBlock.java | 42 ++++
.../org/apache/hudi/io/hfile/HFileDataBlock.java | 27 +-
.../apache/hudi/io/hfile/HFileRootIndexBlock.java | 15 +-
.../apache/hudi/io/hfile/TestHFileDataBlock.java | 79 ++++++
.../hudi/io/hfile/TestHFileReadCompatibility.java | 275 ++++++++++++++++-----
.../hudi/io/hfile/TestHFileRootIndexBlock.java | 80 ++++++
.../org/apache/hudi/io/hfile/TestHFileWriter.java | 114 ++++++++-
8 files changed, 549 insertions(+), 93 deletions(-)
diff --git a/hudi-io/hfile_format.md b/hudi-io/hfile_format.md
index 192c3d4313f8..df96148b9f10 100644
--- a/hudi-io/hfile_format.md
+++ b/hudi-io/hfile_format.md
@@ -197,7 +197,10 @@ Key:
- **Key Content Size**: 2 byte, short, size of the key content.
- **Key Content**: key content in byte array. In Hudi, we serialize the String
into byte array using UTF-8.
-- **Other Information**: other information of the key, which is not used by
Hudi.
+- **Other Information**: the remaining fields that complete the KeyValue key,
so it parses as a
+ standard KeyValue key (and is readable by an HBase HFile reader): 1-byte
column-family length
+ (`0`), 8-byte timestamp (`Long.MAX_VALUE`, the "latest" sentinel), and
1-byte key type (`Put` = 4).
+ Hudi's reader ignores these fields.
Value:
@@ -286,7 +289,10 @@ For Data Index, the "Key Bytes" part has the following
format (same as the key f
- **Key Content Size**: 2 byte, short, size of the key content.
- **Key Content**: key content in byte array. In Hudi, we encode the String
into bytes using UTF-8.
-- **Other Information**: other information of the key, which is not used by
Hudi.
+- **Other Information**: the remaining fields that complete the KeyValue key,
so it parses as a
+ standard KeyValue key (and is readable by an HBase HFile reader): 1-byte
column-family length
+ (`0`), 8-byte timestamp (`Long.MAX_VALUE`, the "latest" sentinel), and
1-byte key type (`Put` = 4).
+ Hudi's reader ignores these fields.
For Meta Index, the "Key Bytes" part is the byte array of the key of the Meta
Block.
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 56f75466dc61..996af2759cdd 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
@@ -36,6 +36,7 @@ import java.nio.ByteBuffer;
import static org.apache.hudi.io.hfile.DataSize.MAGIC_LENGTH;
import static org.apache.hudi.io.hfile.DataSize.SIZEOF_BYTE;
+import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT16;
import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT32;
import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT64;
import static org.apache.hudi.io.util.IOUtils.readInt;
@@ -56,6 +57,12 @@ public abstract class HFileBlock {
static final int CHECKSUM_SIZE = SIZEOF_INT32;
private static final int DEFAULT_BYTES_PER_CHECKSUM = 16 * 1024;
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ // Hudi does not set a version timestamp on key-value pairs, so the latest
timestamp is used.
+ private static final long LATEST_TIMESTAMP = Long.MAX_VALUE;
+ // Key type is constant Put (4) in Hudi.
+ private static final byte KEY_TYPE_PUT = (byte) 4;
+ // KeyValue key suffix beyond the row: column-family length (1) + timestamp
(8) + type (1).
+ private static final int KEY_METADATA_SUFFIX_LENGTH = SIZEOF_BYTE +
SIZEOF_INT64 + SIZEOF_BYTE;
static class Header {
// Format of header is:
@@ -318,6 +325,41 @@ public abstract class HFileBlock {
throw new HoodieException("Only NULL checksum type is supported");
}
+ /**
+ * Returns the serialized length of the KeyValue key for a row: the 2-byte
row-length prefix, the
+ * row, and the 10-byte metadata suffix (column-family length, timestamp,
key type).
+ *
+ * <p>The data block and the root index block both write the full KeyValue
key (not just the row)
+ * so that a reader can parse and point-look-up either block: a point lookup
compares index keys
+ * against data keys, so the two must use byte-identical key encoding.
+ *
+ * @param rowLength length of the row (key content) in bytes.
+ * @return the KeyValue key length.
+ */
+ protected static int keyValueKeyLength(int rowLength) {
+ return SIZEOF_INT16 + rowLength + KEY_METADATA_SUFFIX_LENGTH;
+ }
+
+ /**
+ * Writes the KeyValue key for a row:
+ * {@code [2-byte rowLen][row][1-byte cfLen=0][8-byte ts=LATEST][1-byte
type=Put]}. See
+ * {@link #keyValueKeyLength(int)} for why the data and index blocks share
this encoding.
+ *
+ * @param out output stream to write to.
+ * @param row buffer holding the row (key content) bytes.
+ * @param offset start of the row within {@code row}; a key may be a view
into a larger buffer.
+ * @param rowLength number of row bytes to write; passed explicitly because
a key's backing array
+ * may be larger than its content length.
+ */
+ protected static void writeKey(DataOutputStream out, byte[] row, int offset,
int rowLength)
+ throws IOException {
+ out.writeShort((short) rowLength);
+ out.write(row, offset, rowLength);
+ out.write(0); // column-family length
+ 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.
diff --git a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
index 551cfd2c961b..1da721d0dc56 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
@@ -28,9 +28,6 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
-import static org.apache.hudi.io.hfile.DataSize.SIZEOF_BYTE;
-import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT16;
-import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT64;
import static
org.apache.hudi.io.hfile.HFileReader.SEEK_TO_BEFORE_BLOCK_FIRST_KEY;
import static org.apache.hudi.io.hfile.HFileReader.SEEK_TO_FOUND;
import static org.apache.hudi.io.hfile.HFileReader.SEEK_TO_IN_RANGE;
@@ -40,18 +37,11 @@ import static org.apache.hudi.io.hfile.KeyValue.KEY_OFFSET;
* Represents a {@link HFileBlockType#DATA} block.
*/
public class HFileDataBlock extends HFileBlock {
- private static final int KEY_LENGTH_LENGTH = SIZEOF_INT16;
- private static final int COLUMN_FAMILY_LENGTH = SIZEOF_BYTE;
- private static final int VERSION_TIMESTAMP_LENGTH = SIZEOF_INT64;
- private static final int KEY_TYPE_LENGTH = SIZEOF_BYTE;
// Hudi does not use HFile MVCC timestamp version so the version
// is always 0, thus the byte length of the version is always 1.
// This assumption is also validated when parsing {@link HFileInfo},
// i.e., the maximum MVCC timestamp in a HFile must be 0.
private static final long ZERO_TS_VERSION_BYTE_LENGTH = 1;
- // Hudi does not set version timestamp for key value pairs,
- // so the latest timestamp is used.
- private static final long LATEST_TIMESTAMP = Long.MAX_VALUE;
// End offset of content in the block, relative to the start of the block.
The key-values
// occupy exactly uncompressedSizeWithoutHeader bytes after the header; the
checksum trails
@@ -218,24 +208,11 @@ public class HFileDataBlock extends HFileBlock {
// Length of key + length of a short variable indicating length of key.
// Note that 10 extra bytes are required by hbase reader.
// That is: 1 byte for column family length, 8 bytes for timestamp, 1
bytes for key type.
- dataOutputStream.writeInt(
- kv.key.length + KEY_LENGTH_LENGTH + COLUMN_FAMILY_LENGTH +
VERSION_TIMESTAMP_LENGTH + KEY_TYPE_LENGTH);
+ dataOutputStream.writeInt(keyValueKeyLength(kv.key.length));
// Length of value.
dataOutputStream.writeInt(kv.value.length);
- // Key content length.
- dataOutputStream.writeShort((short)kv.key.length);
// Key.
- dataOutputStream.write(kv.key);
- // Column family length: constant 0.
- dataOutputStream.write(0);
- // Column qualifier: assume 0 bits.
- // Timestamp: using the latest.
- dataOutputStream.writeLong(LATEST_TIMESTAMP);
- // Key type: constant Put (4) in Hudi.
- // Minimum((byte) 0), Put((byte) 4), Delete((byte) 8),
- // DeleteFamilyVersion((byte) 10), DeleteColumn((byte) 12),
- // DeleteFamily((byte) 14), Maximum((byte) 255).
- dataOutputStream.write(4);
+ writeKey(dataOutputStream, kv.key, 0, kv.key.length);
// Value.
dataOutputStream.write(kv.value);
// MVCC.
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 5b32a5df8925..56a06ed1ba32 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
@@ -29,7 +29,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
-import static org.apache.hudi.io.hfile.DataSize.SIZEOF_INT16;
import static org.apache.hudi.io.util.IOUtils.copy;
import static org.apache.hudi.io.util.IOUtils.decodeVarLongSizeOnDisk;
import static org.apache.hudi.io.util.IOUtils.readInt;
@@ -109,15 +108,11 @@ public class HFileRootIndexBlock extends HFileIndexBlock {
outputStream.writeLong(entry.getOffset());
outputStream.writeInt(entry.getSize());
- // Key length + 2 (SIZEOF_INT16 for the 2-byte row key length prefix).
- // Use Hadoop WritableUtils VarInt encoding to match HBase's HFile
format.
- byte[] keyLength = writeVarInt(
- entry.getFirstKey().getLength() + SIZEOF_INT16);
- outputStream.write(keyLength);
- // Key length.
- outputStream.writeShort((short) entry.getFirstKey().getLength());
- // Key.
- outputStream.write(entry.getFirstKey().getBytes());
+ // Use Hadoop WritableUtils VarInt encoding to match HBase's HFile
format and the reader.
+ int kvKeyLength = keyValueKeyLength(entry.getFirstKey().getLength());
+ outputStream.write(writeVarInt(kvKeyLength));
+ Key firstKey = entry.getFirstKey();
+ writeKey(outputStream, firstKey.getBytes(), firstKey.getOffset(),
firstKey.getLength());
}
}
diff --git
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileDataBlock.java
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileDataBlock.java
new file mode 100644
index 000000000000..c040bca9a97e
--- /dev/null
+++ b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileDataBlock.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hudi.io.hfile;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Validates the exact on-disk bytes the data block writer emits for each
record. A data entry is a
+ * full HBase KeyValue: {@code [4-byte keyLen][4-byte valueLen][2-byte
rowLen][row][1-byte cfLen=0]
+ * [8-byte ts=LATEST][1-byte type=Put][value][1-byte MVCC=0]}. An HBase reader
relies on this exact
+ * framing, so the test asserts every field rather than a single opaque blob.
+ */
+class TestHFileDataBlock {
+ private static final long LATEST_TIMESTAMP = Long.MAX_VALUE;
+ private static final byte KEY_TYPE_PUT = (byte) 4;
+ // Column-family length (1) + timestamp (8) + key type (1) + the 2-byte
row-length prefix.
+ private static final int KEY_SUFFIX_AND_PREFIX_LENGTH = 12;
+
+ @Test
+ void writesFullHBaseKeyValuePerRecord() throws IOException {
+ HFileDataBlock block =
+ HFileDataBlock.createDataBlockToWrite(HFileContext.builder().build(),
-1L);
+ // Two records of different key/value lengths to verify the framing
repeats correctly.
+ block.add(utf8("key1"), utf8("value1"));
+ block.add(utf8("k22"), utf8("v2"));
+
+ ByteBuffer buf = block.getUncompressedBlockDataToWrite();
+ assertDataEntry(buf, "key1", "value1");
+ assertDataEntry(buf, "k22", "v2");
+ assertEquals(0, buf.remaining(), "unexpected trailing bytes after the last
record");
+ }
+
+ private static void assertDataEntry(ByteBuffer buf, String key, String
value) {
+ int rowLength = utf8(key).length;
+ int valueLength = utf8(value).length;
+ assertEquals(rowLength + KEY_SUFFIX_AND_PREFIX_LENGTH, buf.getInt(), "key
length for " + key);
+ assertEquals(valueLength, buf.getInt(), "value length for " + key);
+ assertEquals((short) rowLength, buf.getShort(), "row length for " + key);
+ assertEquals(key, readString(buf, rowLength), "row for " + key);
+ assertEquals((byte) 0, buf.get(), "column-family length for " + key);
+ assertEquals(LATEST_TIMESTAMP, buf.getLong(), "timestamp for " + key);
+ assertEquals(KEY_TYPE_PUT, buf.get(), "key type for " + key);
+ assertEquals(value, readString(buf, valueLength), "value for " + key);
+ assertEquals((byte) 0, buf.get(), "MVCC version for " + key);
+ }
+
+ private static byte[] utf8(String s) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String readString(ByteBuffer buf, int length) {
+ byte[] bytes = new byte[length];
+ buf.get(bytes);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+}
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 8ca7ee7e5eab..c190a62fded3 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
@@ -22,6 +22,7 @@ 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.compress.CompressionCodec;
import org.apache.hudi.io.util.IOUtils;
import org.apache.hadoop.conf.Configuration;
@@ -29,19 +30,20 @@ import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellComparatorImpl;
+import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.KeyValue;
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.HFileContext;
import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
import org.apache.hadoop.hbase.io.hfile.HFileScanner;
import org.apache.hadoop.hbase.util.Bytes;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.EnumSource;
+import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
@@ -57,6 +59,11 @@ import static
org.apache.hudi.io.hfile.HFileInfo.KEY_VALUE_VERSION;
import static org.apache.hudi.io.util.FileIOUtils.readAsByteArray;
import static org.apache.hudi.io.util.IOUtils.readInt;
import static org.apache.hudi.io.util.IOUtils.toBytes;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class TestHFileReadCompatibility {
// Test data - simple key-value pairs
@@ -68,6 +75,11 @@ class TestHFileReadCompatibility {
new TestRecord("row5", "value5")
);
+ // Small block size + many records => many data blocks => block-boundary
keys land in the root
+ // index, which is exactly where the HBase point-lookup parsing happened.
+ private static final int MULTI_BLOCK_RECORDS = 2000;
+ private static final int SMALL_BLOCK_SIZE = 512;
+
@ParameterizedTest
@CsvSource({
"/hfile/hudi-generated.hfile,/hfile/hbase-generated.hfile",
@@ -78,8 +90,8 @@ class TestHFileReadCompatibility {
org.apache.hadoop.hbase.io.hfile.HFile.Reader hbaseReader =
createHBaseHFileReaderFromResource(hbaseFilePath)) {
// Validate number of entries.
- Assertions.assertEquals(5, hudiReader.getNumKeyValueEntries());
- Assertions.assertEquals(5, hbaseReader.getEntries());
+ assertEquals(5, hudiReader.getNumKeyValueEntries());
+ assertEquals(5, hbaseReader.getEntries());
// Validate data block content.
hudiReader.seekTo();
HFileScanner scanner = hbaseReader.getScanner(true, true);
@@ -89,60 +101,60 @@ class TestHFileReadCompatibility {
org.apache.hudi.io.hfile.KeyValue keyValue =
hudiReader.getKeyValue().get();
Cell cell = scanner.getCell();
// Ensure Hudi record is correct.
- Assertions.assertEquals(TEST_RECORDS.get(i).key,
keyValue.getKey().getContentInString());
+ assertEquals(TEST_RECORDS.get(i).key,
keyValue.getKey().getContentInString());
byte[] value = Arrays.copyOfRange(
keyValue.getBytes(),
keyValue.getValueOffset(),
keyValue.getValueOffset() + keyValue.getValueLength());
- Assertions.assertArrayEquals(value,
TEST_RECORDS.get(i).value.getBytes());
+ assertArrayEquals(value, TEST_RECORDS.get(i).value.getBytes());
// Ensure Hbase record is correct.
byte[] key = Arrays.copyOfRange(
cell.getRowArray(),
cell.getRowOffset(),
cell.getRowOffset() + cell.getRowLength());
- Assertions.assertArrayEquals(TEST_RECORDS.get(i).key.getBytes(), key);
+ assertArrayEquals(TEST_RECORDS.get(i).key.getBytes(), key);
value = Arrays.copyOfRange(
cell.getValueArray(),
cell.getValueOffset(),
cell.getValueOffset() + cell.getValueLength());
- Assertions.assertArrayEquals(value,
TEST_RECORDS.get(i).value.getBytes());
+ assertArrayEquals(value, TEST_RECORDS.get(i).value.getBytes());
i++;
} while (hudiReader.next() && scanner.next());
// Compare some meta information.
// LAST KEY.
-
Assertions.assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.LAST_KEY.getBytes()));
-
Assertions.assertTrue(hudiReader.getMetaInfo(HFileInfo.LAST_KEY).isPresent());
+
assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.LAST_KEY.getBytes()));
+ assertTrue(hudiReader.getMetaInfo(HFileInfo.LAST_KEY).isPresent());
// The last key value returned from hbase contains the extra fields,
// e.g., column family, column qualifier, timestamp, key type, which is
10 more bytes.
// Therefore, the last key value from hudi should be the prefix since
hudi does not use these
// extra fields.
if (hudiReader.getMetaInfo(HFileInfo.LAST_KEY).get().length
<
hbaseReader.getHFileInfo().get(HFileInfo.LAST_KEY.getBytes()).length) {
- Assertions.assertTrue(isPrefix(
+ assertTrue(isPrefix(
hudiReader.getMetaInfo(HFileInfo.LAST_KEY).get(),
hbaseReader.getHFileInfo().get(HFileInfo.LAST_KEY.getBytes())));
} else {
- Assertions.assertTrue(isPrefix(
+ assertTrue(isPrefix(
hbaseReader.getHFileInfo().get(HFileInfo.LAST_KEY.getBytes()),
hudiReader.getMetaInfo(HFileInfo.LAST_KEY).get()));
}
// Average key length.
-
Assertions.assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.AVG_KEY_LEN.getBytes()));
-
Assertions.assertTrue(hudiReader.getMetaInfo(HFileInfo.AVG_KEY_LEN).isPresent());
+
assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.AVG_KEY_LEN.getBytes()));
+ assertTrue(hudiReader.getMetaInfo(HFileInfo.AVG_KEY_LEN).isPresent());
// Average value length.
-
Assertions.assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.AVG_VALUE_LEN.getBytes()));
-
Assertions.assertTrue(hudiReader.getMetaInfo(HFileInfo.AVG_VALUE_LEN).isPresent());
- Assertions.assertTrue(
+
assertTrue(hbaseReader.getHFileInfo().containsKey(HFileInfo.AVG_VALUE_LEN.getBytes()));
+ assertTrue(hudiReader.getMetaInfo(HFileInfo.AVG_VALUE_LEN).isPresent());
+ assertTrue(
hbaseReader.getHFileInfo().getAvgValueLen()
>=
readInt(hudiReader.getMetaInfo(HFileInfo.AVG_VALUE_LEN).get(), 0));
// MVCC.
-
Assertions.assertTrue(hbaseReader.getHFileInfo().shouldIncludeMemStoreTS());
+ assertTrue(hbaseReader.getHFileInfo().shouldIncludeMemStoreTS());
// Note that MemStoreTS is not set.
- Assertions.assertFalse(hbaseReader.getHFileInfo().isDecodeMemstoreTS());
-
Assertions.assertTrue(hudiReader.getMetaInfo(KEY_VALUE_VERSION).isPresent());
-
Assertions.assertTrue(hudiReader.getMetaInfo(HFileInfo.MAX_MVCC_TS_KEY).isPresent());
- Assertions.assertEquals(0L,
+ assertFalse(hbaseReader.getHFileInfo().isDecodeMemstoreTS());
+ assertTrue(hudiReader.getMetaInfo(KEY_VALUE_VERSION).isPresent());
+
assertTrue(hudiReader.getMetaInfo(HFileInfo.MAX_MVCC_TS_KEY).isPresent());
+ assertEquals(0L,
IOUtils.readLong(
hudiReader.getMetaInfo(HFileInfo.MAX_MVCC_TS_KEY).get(), 0));
}
@@ -160,13 +172,13 @@ class TestHFileReadCompatibility {
// Create HBase HFile.Reader from the temporary file
HFile.Reader reader = HFile.createReader(fs, new
Path(tempFile.toString()), conf);
byte[] keyValueVersion =
reader.getHFileInfo().get(KEY_VALUE_VERSION.getBytes());
- Assertions.assertEquals(1, IOUtils.readInt(keyValueVersion, 0));
+ assertEquals(1, IOUtils.readInt(keyValueVersion, 0));
// Values from trailer still works.
- Assertions.assertEquals(5, reader.getEntries());
+ assertEquals(5, reader.getEntries());
// Scanning the file succeeds.
HFileScanner scanner = reader.getScanner(true, true);
scanner.seekTo();
- Assertions.assertDoesNotThrow(() -> {
+ assertDoesNotThrow(() -> {
int i = 0;
do {
Cell cell = scanner.getCell();
@@ -174,12 +186,118 @@ class TestHFileReadCompatibility {
cell.getRowArray(),
cell.getRowOffset(),
cell.getRowOffset() + cell.getRowLength());
- Assertions.assertArrayEquals(TEST_RECORDS.get(i).key.getBytes(), key);
+ assertArrayEquals(TEST_RECORDS.get(i).key.getBytes(), key);
i++;
} while (scanner.next());
});
}
+ /**
+ * Validates the block-index key encoding in the HFile: an HBase reader
point-looks-up every key
+ * (including the block-boundary keys stored in the root index) in a
native-written multi-block
+ * file and gets an exact match with the correct value.
+ */
+ @ParameterizedTest
+ @EnumSource(value = CompressionCodec.class, names = {"NONE", "GZIP"})
+ void hbaseReaderPointLooksUpEveryKeyInNativeMultiBlockFile(CompressionCodec
codec)
+ throws IOException {
+ byte[] data = writeMultiBlockHudiHFile(MULTI_BLOCK_RECORDS,
SMALL_BLOCK_SIZE, codec);
+ try (HFile.Reader reader = createHBaseHFileReader(data)) {
+ int blocks = reader.getTrailer().getDataIndexCount();
+ assertTrue(blocks > 1, "expected a multi-block file; got " + blocks);
+ assertEquals(MULTI_BLOCK_RECORDS, reader.getEntries());
+ HFileScanner scanner = reader.getScanner(true, true);
+ for (int i = 0; i < MULTI_BLOCK_RECORDS; i++) {
+ KeyValue probe = new KeyValue(Bytes.toBytes(key(i)), null, null, null);
+ assertEquals(0, scanner.seekTo(probe), "expected exact match for " +
key(i));
+ Cell cell = scanner.getCell();
+ assertEquals(key(i),
+ Bytes.toString(cell.getRowArray(), cell.getRowOffset(),
cell.getRowLength()));
+ assertEquals(value(i),
+ Bytes.toString(cell.getValueArray(), cell.getValueOffset(),
cell.getValueLength()));
+ }
+ }
+ }
+
+ /** Validates the block-index key encoding parses as a {@code KeyValue} via
{@code midKey()}. */
+ @Test
+ void hbaseReaderMidKeyParsesNativeBlockIndexKey() throws IOException {
+ byte[] data = writeMultiBlockHudiHFile(MULTI_BLOCK_RECORDS,
SMALL_BLOCK_SIZE, CompressionCodec.NONE);
+ try (HFile.Reader reader = createHBaseHFileReader(data)) {
+ assertTrue(reader.midKey().isPresent());
+ }
+ }
+
+ /**
+ * Byte comparison under both NONE and GZIP: given identical records, the
native writer and the
+ * HBase writer produce cells that an HBase reader sees as byte-identical
(key bytes and value
+ * bytes), establishing that the native writer emits HBase-format cells.
+ */
+ @ParameterizedTest
+ @EnumSource(value = CompressionCodec.class, names = {"NONE", "GZIP"})
+ void
nativeAndHBaseWrittenCellsAreByteIdenticalUnderHBaseReader(CompressionCodec
codec)
+ throws IOException {
+ byte[] nativeData = writeMultiBlockHudiHFile(MULTI_BLOCK_RECORDS,
SMALL_BLOCK_SIZE, codec);
+ byte[] hbaseData =
+ writeMultiBlockHBaseHFile(MULTI_BLOCK_RECORDS, SMALL_BLOCK_SIZE,
hbaseAlgo(codec));
+ try (HFile.Reader nativeReader = createHBaseHFileReader(nativeData);
+ HFile.Reader hbaseReader = createHBaseHFileReader(hbaseData)) {
+ assertEquals(hbaseReader.getEntries(), nativeReader.getEntries());
+ HFileScanner ns = nativeReader.getScanner(true, true);
+ HFileScanner hs = hbaseReader.getScanner(true, true);
+ assertTrue(ns.seekTo());
+ assertTrue(hs.seekTo());
+ int compared = 0;
+ boolean nativeHasNext;
+ boolean hbaseHasNext;
+ do {
+ Cell nativeCell = ns.getCell();
+ Cell hbaseCell = hs.getCell();
+ assertArrayEquals(keyBytes(nativeCell), keyBytes(hbaseCell),
+ "KeyValue key bytes differ at record " + compared);
+ assertArrayEquals(valueBytes(nativeCell), valueBytes(hbaseCell),
+ "value bytes differ at record " + compared);
+ compared++;
+ nativeHasNext = ns.next();
+ hbaseHasNext = hs.next();
+ } while (nativeHasNext && hbaseHasNext);
+ assertEquals(MULTI_BLOCK_RECORDS, compared);
+ assertFalse(nativeHasNext, "native scanner had extra cells");
+ assertFalse(hbaseHasNext, "hbase scanner had extra cells");
+ }
+ }
+
+ /**
+ * Cross-reader equivalence: the same native-written multi-block file reads
identically through
+ * the native hudi-io reader and the HBase reader (same rows and values, in
order).
+ */
+ @Test
+ void nativeWrittenFileReadsIdenticallyByBothReaders() throws IOException {
+ byte[] data = writeMultiBlockHudiHFile(MULTI_BLOCK_RECORDS,
SMALL_BLOCK_SIZE, CompressionCodec.NONE);
+ try (HFileReader nativeReader = createHFileReader(data);
+ HFile.Reader hbaseReader = createHBaseHFileReader(data)) {
+ assertEquals(MULTI_BLOCK_RECORDS, hbaseReader.getEntries());
+ nativeReader.seekTo();
+ HFileScanner hbaseScanner = hbaseReader.getScanner(true, true);
+ assertTrue(hbaseScanner.seekTo());
+ for (int i = 0; i < MULTI_BLOCK_RECORDS; i++) {
+ org.apache.hudi.io.hfile.KeyValue nativeKv =
nativeReader.getKeyValue().get();
+ Cell hbaseCell = hbaseScanner.getCell();
+ assertEquals(key(i), nativeKv.getKey().getContentInString());
+ assertEquals(key(i),
+ Bytes.toString(hbaseCell.getRowArray(), hbaseCell.getRowOffset(),
hbaseCell.getRowLength()));
+ byte[] nativeValue = Arrays.copyOfRange(nativeKv.getBytes(),
nativeKv.getValueOffset(),
+ nativeKv.getValueOffset() + nativeKv.getValueLength());
+ assertArrayEquals(value(i).getBytes(StandardCharsets.UTF_8),
nativeValue);
+ assertArrayEquals(nativeValue, valueBytes(hbaseCell));
+ if (i < MULTI_BLOCK_RECORDS - 1) {
+ assertTrue(nativeReader.next());
+ assertTrue(hbaseScanner.next());
+ }
+ }
+ }
+ }
+
static boolean isPrefix(byte[] prefix, byte[] array) {
if (prefix.length > array.length) {
return false; // can't be prefix if longer
@@ -193,39 +311,25 @@ class TestHFileReadCompatibility {
}
static HFileReader createHFileReaderFromResource(String fileName) throws
IOException {
- // Read HFile data from resources
- byte[] hfileData = readHFileFromResources(fileName);
- // Convert to ByteBuffer
- ByteBuffer buffer = ByteBuffer.wrap(hfileData);
- // Create SeekableDataInputStream
+ return createHFileReader(readHFileFromResources(fileName));
+ }
+
+ static HFileReader createHFileReader(byte[] hfileData) {
SeekableDataInputStream inputStream = new ByteArraySeekableDataInputStream(
- new ByteBufferBackedInputStream(buffer)
- );
- // Create and return HFileReaderImpl
+ new ByteBufferBackedInputStream(ByteBuffer.wrap(hfileData)));
return new HFileReaderImpl(inputStream, hfileData.length);
}
static HFile.Reader createHBaseHFileReaderFromResource(String fileName)
throws IOException {
- // Read HFile data from resources
- byte[] hfileData = readHFileFromResources(fileName);
- // Create a temporary file to write the HFile data
+ return createHBaseHFileReader(readHFileFromResources(fileName));
+ }
+
+ static HFile.Reader createHBaseHFileReader(byte[] hfileData) throws
IOException {
Path tempFile = new Path(Files.createTempFile("hbase_hfile_",
".hfile").toString());
- try {
- // Write the byte array to temporary file
- Files.write(Paths.get(tempFile.toString()), hfileData);
- // Create Hadoop Configuration and FileSystem
- Configuration conf = new Configuration();
- FileSystem fs = FileSystem.get(conf);
- // Create HBase HFile.Reader from the temporary file
- HFile.Reader reader = HFile.createReader(fs, new
Path(tempFile.toString()), conf);
- // Note: The temporary file will be cleaned up when the reader is closed
- // or you can manually delete it after use
- return reader;
- } catch (IOException e) {
- // Clean up temp file if creation fails
- Files.deleteIfExists(Paths.get(tempFile.toString()));
- throw e;
- }
+ Files.write(Paths.get(tempFile.toString()), hfileData);
+ Configuration conf = new Configuration();
+ FileSystem fs = FileSystem.get(conf);
+ return HFile.createReader(fs, tempFile, conf);
}
private static byte[] readHFileFromResources(String filename) throws
IOException {
@@ -250,7 +354,7 @@ class TestHFileReadCompatibility {
FileSystem fs = FileSystem.get(conf);
// Create HFile context with appropriate settings
- HFileContext context = new HFileContextBuilder()
+ org.apache.hadoop.hbase.io.hfile.HFileContext context = new
HFileContextBuilder()
.withBlockSize(64 * 1024)
.withCompression(Compression.Algorithm.NONE)
.withCellComparator(CellComparatorImpl.COMPARATOR)
@@ -279,7 +383,7 @@ class TestHFileReadCompatibility {
}
private void writeHFileWithHudi(Path filePath, int keyValueVersion) throws
IOException {
- org.apache.hudi.io.hfile.HFileContext context =
org.apache.hudi.io.hfile.HFileContext.builder()
+ HFileContext context = HFileContext.builder()
.blockSize(64 * 1024)
.build();
try (DataOutputStream outputStream = new DataOutputStream(
@@ -297,6 +401,67 @@ class TestHFileReadCompatibility {
}
}
+ private static String key(int i) {
+ return String.format("key%06d", i);
+ }
+
+ private static String value(int i) {
+ return "value-" + i;
+ }
+
+ private static Compression.Algorithm hbaseAlgo(CompressionCodec codec) {
+ return codec == CompressionCodec.GZIP ? Compression.Algorithm.GZ :
Compression.Algorithm.NONE;
+ }
+
+ private static byte[] writeMultiBlockHudiHFile(int numRecords, int
blockSize, CompressionCodec codec)
+ throws IOException {
+ HFileContext context = HFileContext.builder()
+ .blockSize(blockSize).compressionCodec(codec).build();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (HFileWriter writer = new HFileWriterImpl(context, baos)) {
+ for (int i = 0; i < numRecords; i++) {
+ writer.append(key(i), value(i).getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ return baos.toByteArray();
+ }
+
+ // Same cells the native writer emits: family length 0, timestamp = LATEST,
type = Put.
+ private static byte[] writeMultiBlockHBaseHFile(int numRecords, int
blockSize,
+ Compression.Algorithm algo)
throws IOException {
+ Configuration conf = new Configuration();
+ FileSystem fs = FileSystem.getLocal(conf);
+ Path path = new Path(Files.createTempFile("hbase_write_",
".hfile").toString());
+ org.apache.hadoop.hbase.io.hfile.HFileContext context = new
HFileContextBuilder()
+ .withBlockSize(blockSize)
+ .withCompression(algo)
+ .withCellComparator(CellComparatorImpl.COMPARATOR)
+ .withIncludesMvcc(true)
+ .build();
+ try (HFile.Writer writer = HFile.getWriterFactory(conf, new
CacheConfig(conf))
+ .withPath(fs, path).withFileContext(context).create()) {
+ for (int i = 0; i < numRecords; i++) {
+ writer.append(new KeyValue(Bytes.toBytes(key(i)), new byte[0], new
byte[0],
+ HConstants.LATEST_TIMESTAMP,
value(i).getBytes(StandardCharsets.UTF_8)));
+ }
+ }
+ return Files.readAllBytes(Paths.get(path.toString()));
+ }
+
+ private static byte[] keyBytes(Cell c) {
+ KeyValue kv = new KeyValue(c.getRowArray(), c.getRowOffset(),
c.getRowLength(),
+ c.getFamilyArray(), c.getFamilyOffset(), c.getFamilyLength(),
+ c.getQualifierArray(), c.getQualifierOffset(), c.getQualifierLength(),
+ c.getTimestamp(), KeyValue.Type.codeToType(c.getTypeByte()),
+ c.getValueArray(), c.getValueOffset(), c.getValueLength());
+ return Arrays.copyOfRange(kv.getKey(), 0, kv.getKeyLength());
+ }
+
+ private static byte[] valueBytes(Cell c) {
+ return Arrays.copyOfRange(c.getValueArray(), c.getValueOffset(),
+ c.getValueOffset() + c.getValueLength());
+ }
+
// Simple test record class
private static class TestRecord {
final String key;
diff --git
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileRootIndexBlock.java
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileRootIndexBlock.java
new file mode 100644
index 000000000000..bf24801ea258
--- /dev/null
+++
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileRootIndexBlock.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hudi.io.hfile;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Validates the exact on-disk bytes the root index block writer emits for
each entry. An entry is
+ * {@code [8-byte offset][4-byte size][varint keyLen][2-byte
rowLen][row][1-byte cfLen=0]
+ * [8-byte ts=LATEST][1-byte type=Put]}: the block-index "first key" is a full
HBase KeyValue key,
+ * byte-identical to the data block's key, so an HBase reader can
point-look-up the block index.
+ */
+class TestHFileRootIndexBlock {
+ private static final long LATEST_TIMESTAMP = Long.MAX_VALUE;
+ private static final byte KEY_TYPE_PUT = (byte) 4;
+ // Column-family length (1) + timestamp (8) + key type (1) + the 2-byte
row-length prefix.
+ private static final int KEY_SUFFIX_AND_PREFIX_LENGTH = 12;
+
+ @Test
+ void writesFullHBaseKeyValueKeyPerEntry() throws IOException {
+ HFileRootIndexBlock block =
+
HFileRootIndexBlock.createRootIndexBlockToWrite(HFileContext.builder().build());
+ // Two entries of different key lengths, offsets, and sizes.
+ block.add(utf8("key1"), 0L, 100);
+ block.add(utf8("key0002"), 100L, 250);
+
+ ByteBuffer buf = block.getUncompressedBlockDataToWrite();
+ assertIndexEntry(buf, "key1", 0L, 100);
+ assertIndexEntry(buf, "key0002", 100L, 250);
+ assertEquals(0, buf.remaining(), "unexpected trailing bytes after the last
entry");
+ }
+
+ private static void assertIndexEntry(ByteBuffer buf, String key, long
offset, int size) {
+ int rowLength = utf8(key).length;
+ assertEquals(offset, buf.getLong(), "offset for " + key);
+ assertEquals(size, buf.getInt(), "size for " + key);
+ // The key length is a Hadoop WritableUtils VarInt; it is a single byte
here because
+ // rowLength + 12 < 128 for these keys (multi-byte VarInt is covered by
long-key read tests).
+ assertEquals(rowLength + KEY_SUFFIX_AND_PREFIX_LENGTH, buf.get() & 0xff,
+ "varint key length for " + key);
+ assertEquals((short) rowLength, buf.getShort(), "row length for " + key);
+ assertEquals(key, readString(buf, rowLength), "row for " + key);
+ assertEquals((byte) 0, buf.get(), "column-family length for " + key);
+ assertEquals(LATEST_TIMESTAMP, buf.getLong(), "timestamp for " + key);
+ assertEquals(KEY_TYPE_PUT, buf.get(), "key type for " + key);
+ }
+
+ private static byte[] utf8(String s) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String readString(ByteBuffer buf, int length) {
+ byte[] bytes = new byte[length];
+ buf.get(bytes);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+}
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 a4488351c7e2..e1995684c88d 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
@@ -24,6 +24,14 @@ import org.apache.hudi.io.ByteBufferBackedInputStream;
import org.apache.hudi.io.SeekableDataInputStream;
import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.hbase.CellComparatorImpl;
+import org.apache.hadoop.hbase.HConstants;
+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.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -55,6 +63,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class TestHFileWriter {
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
+ // and the root block-index block. Update intentionally ONLY after reviewing
HBase-reader
+ // compatibility, since any change here is a storage-format change.
+ private static final String GOLDEN_DATA_REGION_HEX =
+
"44415441424c4b2a000000610000005dffffffffffffffff00000040000000007e000000100000000600046b65793100"
+ +
"7fffffffffffffff0476616c75653100000000100000000600046b657932007fffffffffffffff0476616c7565320000"
+ +
"0000100000000600046b657933007fffffffffffffff0476616c7565330000000000";
+ private static final String GOLDEN_ROOT_INDEX_BLOCK_HEX =
+
"494458524f4f5432000000210000001dffffffffffffffff00000040000000003e000000000000000000000082100004"
+ + "6b657931007fffffffffffffff0400000000";
@AfterEach
public void tearDown() throws IOException {
@@ -232,6 +250,72 @@ class TestHFileWriter {
}
}
+ /**
+ * 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.
+ */
+ @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");
+ }
+
+ /** Returns {@code [dataRegionHex, rootIndexBlockHex]} for an HFile's raw
bytes. */
+ private static String[] dataAndRootIndexBlockHex(byte[] data) {
+ int idxRootOffset = indexOf(data, HFileBlockType.ROOT_INDEX.getMagic());
+ assertTrue(idxRootOffset > 0, "root index block not found");
+ // Root index block: 33-byte v3 block header + onDiskSizeWithoutHeader
payload (at header + 8).
+ int onDiskSizeWithoutHeader = readIntBE(data, idxRootOffset + 8);
+ return new String[] {
+ hex(Arrays.copyOfRange(data, 0, idxRootOffset)),
+ hex(Arrays.copyOfRange(data, idxRootOffset, idxRootOffset + 33 +
onDiskSizeWithoutHeader))
+ };
+ }
+
+ /** Writes key1/key2/key3 with the HBase HFile writer, matching the native
writer's settings. */
+ private static byte[] writeFixedHBaseFile() throws IOException {
+ Configuration conf = new Configuration();
+ FileSystem fs = FileSystem.getLocal(conf);
+ org.apache.hadoop.fs.Path path =
+ new org.apache.hadoop.fs.Path(Files.createTempFile("hbase_fixed_",
".hfile").toString());
+ org.apache.hadoop.hbase.io.hfile.HFileContext context = new
HFileContextBuilder()
+ .withBlockSize(1024 * 1024)
+ .withCompression(Compression.Algorithm.NONE)
+ .withChecksumType(org.apache.hadoop.hbase.util.ChecksumType.NULL)
+ .withCellComparator(CellComparatorImpl.COMPARATOR)
+ .withIncludesMvcc(true)
+ .build();
+ try (HFile.Writer writer = HFile.getWriterFactory(conf, new
CacheConfig(conf))
+ .withPath(fs, path).withFileContext(context).create()) {
+ for (int i = 1; i <= 3; 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)));
+ }
+ }
+ return Files.readAllBytes(Paths.get(path.toString()));
+ }
+
private static void writeTestFile() throws Exception {
try (
DataOutputStream outputStream =
@@ -246,7 +330,9 @@ class TestHFileWriter {
private static void validateHFileSize() throws IOException {
Path path = Paths.get(TEST_FILE);
long actualSize = Files.size(path);
- long expectedSize = 4537;
+ // Each root block-index entry carries the 10-byte HBase KeyValue suffix
(column-family
+ // length + timestamp + key type). This file has one index entry, so the
size grows by 10.
+ long expectedSize = 4547;
assertEquals(expectedSize, actualSize);
}
@@ -339,6 +425,32 @@ class TestHFileWriter {
}
}
+ private static int indexOf(byte[] haystack, byte[] needle) {
+ outer:
+ for (int i = 0; i + needle.length <= haystack.length; i++) {
+ for (int j = 0; j < needle.length; j++) {
+ if (haystack[i + j] != needle[j]) {
+ continue outer;
+ }
+ }
+ return i;
+ }
+ return -1;
+ }
+
+ private static int readIntBE(byte[] b, int off) {
+ return ((b[off] & 0xff) << 24) | ((b[off + 1] & 0xff) << 16)
+ | ((b[off + 2] & 0xff) << 8) | (b[off + 3] & 0xff);
+ }
+
+ private static String hex(byte[] b) {
+ StringBuilder sb = new StringBuilder(b.length * 2);
+ for (byte x : b) {
+ sb.append(Character.forDigit((x >> 4) & 0xf,
16)).append(Character.forDigit(x & 0xf, 16));
+ }
+ return sb.toString();
+ }
+
public static String generateRandomStringStream(int length) {
String characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();