This is an automated email from the ASF dual-hosted git repository.
charlesconnell pushed a commit to branch branch-2
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-2 by this push:
new 623b51091c3 HBASE-30321: Implement GzipByteBuffDecompressor with
on-heap and off-heap decompression paths (#8541)
623b51091c3 is described below
commit 623b51091c33870456ef646059e165a94bff6161
Author: Saad Ahmad Sabri <[email protected]>
AuthorDate: Tue Aug 18 18:50:23 2026 -0400
HBASE-30321: Implement GzipByteBuffDecompressor with on-heap and off-heap
decompression paths (#8541)
Signed-off by: Charles Connell <[email protected]>
---
.../io/compress/GzipByteBuffDecompressor.java | 136 ++++++++
.../io/compress/GzipHFileDecompressionContext.java | 68 ++++
.../hbase/io/compress/ReusableStreamGzipCodec.java | 21 +-
.../io/compress/TestGzipByteBuffDecompressor.java | 384 +++++++++++++++++++++
.../io/compress/TestHFileCompressionGzip.java | 67 ++++
5 files changed, 674 insertions(+), 2 deletions(-)
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipByteBuffDecompressor.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipByteBuffDecompressor.java
new file mode 100644
index 00000000000..b11af0ec3c5
--- /dev/null
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipByteBuffDecompressor.java
@@ -0,0 +1,136 @@
+/*
+ * 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.hadoop.hbase.io.compress;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.hadoop.hbase.nio.ByteBuff;
+import org.apache.hadoop.hbase.nio.SingleByteBuff;
+import org.apache.hadoop.io.compress.zlib.ZlibDecompressor;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Glue for ByteBuffDecompressor on top of Hadoop's native
+ * {@link ZlibDecompressor.ZlibDirectDecompressor}. Only direct-to-direct
decompression is
+ * supported, which is zero-copy; callers with on-heap buffers fall back to
the stream path.
+ */
[email protected]
+public class GzipByteBuffDecompressor implements ByteBuffDecompressor {
+
+ private static final int GZIP_HEADER_LENGTH = 10;
+ private static final int GZIP_TRAILER_LENGTH = 8;
+
+ @Nullable
+ private final ZlibDecompressor.ZlibDirectDecompressor decompressor;
+
+ private boolean allowByteBuffDecompression;
+
+ GzipByteBuffDecompressor(boolean nativeZlibLoaded) {
+ decompressor = nativeZlibLoaded
+ ? new ZlibDecompressor.ZlibDirectDecompressor(
+ ZlibDecompressor.CompressionHeader.AUTODETECT_GZIP_ZLIB, 0)
+ : null;
+ allowByteBuffDecompression = true;
+ }
+
+ @Override
+ public boolean canDecompress(ByteBuff output, ByteBuff input) {
+ if (!allowByteBuffDecompression) {
+ return false;
+ }
+ if (!(output instanceof SingleByteBuff) || !(input instanceof
SingleByteBuff)) {
+ return false;
+ }
+ // Only direct-to-direct decompression is supported.
+ return input.nioByteBuffers()[0].isDirect() &&
output.nioByteBuffers()[0].isDirect()
+ && decompressor != null;
+ }
+
+ @Override
+ public int decompress(ByteBuff output, ByteBuff input, int inputLen) throws
IOException {
+ if (!(output instanceof SingleByteBuff) || !(input instanceof
SingleByteBuff)) {
+ throw new IllegalStateException(
+ "At least one buffer is not a SingleByteBuff, this is not supported");
+ }
+ if (inputLen < GZIP_HEADER_LENGTH + GZIP_TRAILER_LENGTH) {
+ throw new IOException("Input of length " + inputLen + " is too short to
be a gzip member");
+ }
+
+ ByteBuffer nioInput = input.nioByteBuffers()[0];
+ ByteBuffer nioOutput = output.nioByteBuffers()[0];
+ if (!nioInput.isDirect() || !nioOutput.isDirect() || decompressor == null)
{
+ throw new IllegalStateException(
+ "GzipByteBuffDecompressor only supports direct-to-direct decompression
with native zlib "
+ + "loaded, this should never happen since canDecompress() would have
returned false");
+ }
+ return decompressOffHeap(nioInput, nioOutput, inputLen);
+ }
+
+ private int decompressOffHeap(ByteBuffer nioInput, ByteBuffer nioOutput, int
inputLen)
+ throws IOException {
+ int inputStart = nioInput.position();
+ int outputStart = nioOutput.position();
+
+ ByteBuffer gzipMember = nioInput.duplicate();
+ gzipMember.limit(inputStart + inputLen);
+
+ decompressor.reset();
+ try {
+ decompressor.decompress(gzipMember, nioOutput);
+ } catch (IOException e) {
+ throw new IOException("Invalid gzip stream: " + e.getMessage(), e);
+ }
+ if (!decompressor.finished()) {
+ if (!nioOutput.hasRemaining()) {
+ throw new IOException("Output buffer is too small for the decompressed
gzip stream");
+ }
+ throw new IOException("Unexpected end of gzip stream");
+ }
+ if (gzipMember.hasRemaining()) {
+ throw new IOException("Unexpected trailing bytes after decompressing
gzip stream");
+ }
+
+ nioInput.position(inputStart + inputLen);
+
+ return nioOutput.position() - outputStart;
+ }
+
+ @Override
+ public void reinit(@Nullable Compression.HFileDecompressionContext
newHFileDecompressionContext) {
+ if (newHFileDecompressionContext == null) {
+ return;
+ }
+ if (!(newHFileDecompressionContext instanceof
GzipHFileDecompressionContext)) {
+ throw new IllegalArgumentException(
+ "GzipByteBuffDecompressor#reinit() was given an
HFileDecompressionContext that was not "
+ + "a GzipHFileDecompressionContext, this should never happen");
+ }
+ GzipHFileDecompressionContext gzipContext =
+ (GzipHFileDecompressionContext) newHFileDecompressionContext;
+ allowByteBuffDecompression = gzipContext.isAllowByteBuffDecompression();
+ }
+
+ @Override
+ public void close() {
+ if (decompressor != null) {
+ decompressor.end();
+ }
+ }
+
+}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipHFileDecompressionContext.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipHFileDecompressionContext.java
new file mode 100644
index 00000000000..69bdc7ed10b
--- /dev/null
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/GzipHFileDecompressionContext.java
@@ -0,0 +1,68 @@
+/*
+ * 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.hadoop.hbase.io.compress;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.util.ClassSize;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Holds HFile-level settings used by GzipByteBuffDecompressor. It's expensive
to pull these from a
+ * Configuration object every time we decompress a block, so pull them upon
opening an HFile, and
+ * reuse them in every block that gets decompressed.
+ */
[email protected]
+public final class GzipHFileDecompressionContext extends
Compression.HFileDecompressionContext {
+
+ public static final long FIXED_OVERHEAD =
+ ClassSize.estimateBase(GzipHFileDecompressionContext.class, false);
+
+ public static final String ALLOW_BYTE_BUFF_DECOMPRESSION_KEY =
+ "hbase.io.compress.gz.allowByteBuffDecompression";
+
+ private final boolean allowByteBuffDecompression;
+
+ private GzipHFileDecompressionContext(boolean allowByteBuffDecompression) {
+ this.allowByteBuffDecompression = allowByteBuffDecompression;
+ }
+
+ public boolean isAllowByteBuffDecompression() {
+ return allowByteBuffDecompression;
+ }
+
+ public static GzipHFileDecompressionContext fromConfiguration(Configuration
conf) {
+ return new GzipHFileDecompressionContext(
+ conf.getBoolean(ALLOW_BYTE_BUFF_DECOMPRESSION_KEY, true));
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public long heapSize() {
+ return FIXED_OVERHEAD;
+ }
+
+ @Override
+ public String toString() {
+ return "GzipHFileDecompressionContext{allowByteBuffDecompression=" +
allowByteBuffDecompression
+ + '}';
+ }
+}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/ReusableStreamGzipCodec.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/ReusableStreamGzipCodec.java
index 23aac29981c..08276d7f073 100644
---
a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/ReusableStreamGzipCodec.java
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/compress/ReusableStreamGzipCodec.java
@@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.GZIPOutputStream;
+import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.util.JVM;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.io.compress.CompressorStream;
@@ -35,9 +36,9 @@ import org.slf4j.LoggerFactory;
* Fixes an inefficiency in Hadoop's Gzip codec, allowing to reuse compression
streams.
*/
@InterfaceAudience.Private
-public class ReusableStreamGzipCodec extends GzipCodec {
+public class ReusableStreamGzipCodec extends GzipCodec implements
ByteBuffDecompressionCodec {
- private static final Logger LOG =
LoggerFactory.getLogger(ReusableStreamGzipCodec.class);
+ private static final Logger LOG = LoggerFactory.getLogger(Compression.class);
/**
* A bridge that wraps around a DeflaterOutputStream to make it a
CompressionOutputStream.
@@ -185,4 +186,20 @@ public class ReusableStreamGzipCodec extends GzipCodec {
return new ReusableGzipOutputStream(out);
}
+ @Override
+ public ByteBuffDecompressor createByteBuffDecompressor() {
+ return new
GzipByteBuffDecompressor(ZlibFactory.isNativeZlibLoaded(getConf()));
+ }
+
+ @Override
+ public Class<? extends ByteBuffDecompressor> getByteBuffDecompressorType() {
+ return GzipByteBuffDecompressor.class;
+ }
+
+ @Override
+ public Compression.HFileDecompressionContext
+ getDecompressionContextFromConfiguration(Configuration conf) {
+ return GzipHFileDecompressionContext.fromConfiguration(conf);
+ }
+
}
diff --git
a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/compress/TestGzipByteBuffDecompressor.java
b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/compress/TestGzipByteBuffDecompressor.java
new file mode 100644
index 00000000000..2b86c0ab042
--- /dev/null
+++
b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/compress/TestGzipByteBuffDecompressor.java
@@ -0,0 +1,384 @@
+/*
+ * 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.hadoop.hbase.io.compress;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.zip.GZIPOutputStream;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.nio.ByteBuff;
+import org.apache.hadoop.hbase.nio.MultiByteBuff;
+import org.apache.hadoop.hbase.nio.SingleByteBuff;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.util.NativeCodeLoader;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag(SmallTests.TAG)
+public class TestGzipByteBuffDecompressor {
+
+ // A single gzip member, reused as decompressor input across the tests.
+ private static final byte[] COMPRESSED_PAYLOAD = gzip("HBase is fun to use
and very fast");
+
+ /**
+ * GzipByteBuffDecompressor is backed by Hadoop's native zlib binding, so
actually decompressing
+ * anything requires that native library to be loaded on this JVM.
+ */
+ private static void assumeNativeZlibLoaded() {
+ assumeTrue(NativeCodeLoader.isNativeCodeLoaded(),
+ "Hadoop's native code is not loaded on this JVM, skipping");
+ }
+
+ @Test
+ public void itReportsCorrectCapabilitiesWithoutNativeZlib() {
+ ByteBuff emptySingleDirectBuff = new
SingleByteBuff(ByteBuffer.allocateDirect(0));
+ ByteBuff emptySingleHeapBuff = new SingleByteBuff(ByteBuffer.allocate(0));
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(false)) {
+ assertFalse(decompressor.canDecompress(emptySingleDirectBuff,
emptySingleDirectBuff),
+ "Without native zlib, direct-to-direct decompression is not
available");
+ assertFalse(decompressor.canDecompress(emptySingleHeapBuff,
emptySingleHeapBuff),
+ "Heap decompression is not supported; only direct-to-direct");
+ }
+ }
+
+ @Test
+ public void itReportsCorrectCapabilitiesWithNativeZlib() {
+ assumeNativeZlibLoaded();
+ ByteBuff emptySingleHeapBuff = new SingleByteBuff(ByteBuffer.allocate(0));
+ ByteBuff emptyMultiHeapBuff = new MultiByteBuff(ByteBuffer.allocate(0),
ByteBuffer.allocate(0));
+ ByteBuff emptySingleDirectBuff = new
SingleByteBuff(ByteBuffer.allocateDirect(0));
+ ByteBuff emptyMultiDirectBuff =
+ new MultiByteBuff(ByteBuffer.allocateDirect(0),
ByteBuffer.allocateDirect(0));
+
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ assertTrue(decompressor.canDecompress(emptySingleDirectBuff,
emptySingleDirectBuff));
+ // Only direct-to-direct is supported; heap and mixed buffers return
false.
+ assertFalse(decompressor.canDecompress(emptySingleHeapBuff,
emptySingleHeapBuff));
+ assertFalse(decompressor.canDecompress(emptySingleHeapBuff,
emptySingleDirectBuff));
+ assertFalse(decompressor.canDecompress(emptySingleDirectBuff,
emptySingleHeapBuff));
+ assertFalse(decompressor.canDecompress(emptyMultiHeapBuff,
emptyMultiHeapBuff));
+ assertFalse(decompressor.canDecompress(emptyMultiDirectBuff,
emptyMultiDirectBuff));
+ assertFalse(decompressor.canDecompress(emptySingleDirectBuff,
emptyMultiDirectBuff));
+ }
+ }
+
+ private static ByteBuff directBuffWith(byte[] data) {
+ ByteBuffer buffer = ByteBuffer.allocateDirect(data.length);
+ buffer.put(data);
+ buffer.rewind();
+ return new SingleByteBuff(buffer);
+ }
+
+ private static ByteBuff heapBuffWith(byte[] data) {
+ ByteBuffer buffer = ByteBuffer.allocate(data.length);
+ buffer.put(data);
+ buffer.rewind();
+ return new SingleByteBuff(buffer);
+ }
+
+ private static byte[] gzip(String text) {
+ ByteArrayOutputStream compressed = new ByteArrayOutputStream();
+ try (GZIPOutputStream out = new GZIPOutputStream(compressed)) {
+ out.write(Bytes.toBytes(text));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return compressed.toByteArray();
+ }
+
+ private static byte[] concat(byte[] first, byte[] second) {
+ byte[] combined = new byte[first.length + second.length];
+ System.arraycopy(first, 0, combined, 0, first.length);
+ System.arraycopy(second, 0, combined, first.length, second.length);
+ return combined;
+ }
+
+ @Test
+ public void itDecompressesDirectToDirectSuccessfully() throws IOException {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+ int decompressedSize = decompressor.decompress(output, input,
COMPRESSED_PAYLOAD.length);
+ assertEquals("HBase is fun to use and very fast",
+ Bytes.toString(output.toBytes(0, decompressedSize)));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsOnTooShortInput() throws IOException {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = new SingleByteBuff(ByteBuffer.allocateDirect(10));
+ decompressor.decompress(output, input, 10);
+ fail("Expected an IOException because the input is too short to be a
gzip member");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("too short to be a gzip member"));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsOnBadMagicBytes() throws IOException {
+ assumeNativeZlibLoaded();
+ byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD,
COMPRESSED_PAYLOAD.length);
+ corrupted[0] ^= (byte) 0xff;
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(corrupted);
+ decompressor.decompress(output, input, corrupted.length);
+ fail("Expected an IOException because the magic bytes are wrong");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Invalid gzip stream"));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsWhenOutputBufferTooSmall() throws
IOException {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(10));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+ decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length);
+ fail("Expected an IOException because the output buffer is too small");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Output buffer is too small"));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsOnCorruptedCrc32() throws IOException {
+ assumeNativeZlibLoaded();
+ byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD,
COMPRESSED_PAYLOAD.length);
+ // First 4 bytes of the 8-byte trailer are the CRC32, leave ISIZE (the
last 4 bytes) alone.
+ corrupted[corrupted.length - 8] ^= (byte) 0xff;
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(corrupted);
+ decompressor.decompress(output, input, corrupted.length);
+ fail("Expected an IOException because the trailer's CRC32 no longer
matches");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Invalid gzip stream"));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsOnCorruptedIsize() throws IOException {
+ assumeNativeZlibLoaded();
+ byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD,
COMPRESSED_PAYLOAD.length);
+ // Last 4 bytes of the 8-byte trailer are the ISIZE.
+ corrupted[corrupted.length - 4] ^= (byte) 0xff;
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(corrupted);
+ decompressor.decompress(output, input, corrupted.length);
+ fail("Expected an IOException because the trailer's ISIZE no longer
matches");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Invalid gzip stream"));
+ }
+ }
+
+ @Test
+ public void itDecompressesDirectSuccessfullyOnRepeatedCalls() throws
IOException {
+ assumeNativeZlibLoaded();
+ // Mirrors how CodecPool actually uses these: one instance is reused
across many blocks, so the
+ // native decompressor must produce a correct result on every call, not
just the first.
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ for (int i = 0; i < 3; i++) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+ int decompressedSize = decompressor.decompress(output, input,
COMPRESSED_PAYLOAD.length);
+ assertEquals("HBase is fun to use and very fast",
+ Bytes.toString(output.toBytes(0, decompressedSize)));
+ }
+ }
+ }
+
+ @Test
+ public void itDecompressDirectIsStillUsableAfterAPreviousCallThrows() throws
IOException {
+ assumeNativeZlibLoaded();
+ byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD,
COMPRESSED_PAYLOAD.length);
+ // First 4 bytes of the 8-byte trailer are the CRC32.
+ corrupted[corrupted.length - 8] ^= (byte) 0xff;
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff badOutput = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff badInput = directBuffWith(corrupted);
+ try {
+ decompressor.decompress(badOutput, badInput, corrupted.length);
+ fail("Expected an IOException because the trailer's CRC32 no longer
matches");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Invalid gzip stream"));
+ }
+
+ // A prior failure must not leave the shared native decompressor state
corrupted for the
+ // next, valid call.
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+ int decompressedSize = decompressor.decompress(output, input,
COMPRESSED_PAYLOAD.length);
+ assertEquals("HBase is fun to use and very fast",
+ Bytes.toString(output.toBytes(0, decompressedSize)));
+ }
+ }
+
+ @Test
+ public void itDecompressesDirectToDirectWithNonZeroBufferPosition() throws
IOException {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuffer rawOutput = ByteBuffer.allocateDirect(128);
+ rawOutput.position(32);
+
+ ByteBuffer rawInput = ByteBuffer.allocateDirect(16 +
COMPRESSED_PAYLOAD.length);
+ for (int i = 0; i < 16; i++) {
+ rawInput.put((byte) 0);
+ }
+ rawInput.put(COMPRESSED_PAYLOAD);
+ rawInput.position(16);
+
+ ByteBuff output = new SingleByteBuff(rawOutput);
+ ByteBuff input = new SingleByteBuff(rawInput);
+ int decompressedSize = decompressor.decompress(output, input,
COMPRESSED_PAYLOAD.length);
+
+ byte[] result = new byte[decompressedSize];
+ rawOutput.position(32);
+ rawOutput.get(result);
+ assertEquals("HBase is fun to use and very fast",
Bytes.toString(result));
+ }
+ }
+
+ @Test
+ public void itDecompressDirectFailsOnTruncatedGzipStream() throws
IOException {
+ assumeNativeZlibLoaded();
+ byte[] truncated = Arrays.copyOf(COMPRESSED_PAYLOAD,
COMPRESSED_PAYLOAD.length - 4);
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(truncated);
+ decompressor.decompress(output, input, truncated.length);
+ fail("Expected an IOException because the gzip stream is truncated");
+ } catch (IOException e) {
+ // Expected: the decompressor must not report finished() on an
incomplete stream
+ }
+ }
+
+ @Test
+ public void itDecompressesOnlyTheDelimitedMemberFromAMultiMemberPayload()
throws IOException {
+ assumeNativeZlibLoaded();
+ // Two distinct members concatenated: we must decode only the one
delimited by inputLen.
+ byte[] firstMember = gzip("first member");
+ byte[] secondMember = gzip("second member");
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(concat(firstMember, secondMember));
+ int decompressedSize = decompressor.decompress(output, input,
firstMember.length);
+ assertEquals("first member", Bytes.toString(output.toBytes(0,
decompressedSize)));
+ }
+ }
+
+ /**
+ * This is the exact gate {@code
HFileBlockDefaultDecodingContext#canDecompressViaByteBuff} relies
+ * on to decide between ByteBuff decompression and the stream path, driven
end-to-end from the
+ * {@code GzipHFileDecompressionContext#ALLOW_BYTE_BUFF_DECOMPRESSION_KEY}
config flag.
+ */
+ @Test
+ public void itReinitControlsByteBuffDecompressionViaConfigFlag() {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+
+ Configuration conf = new Configuration(false);
+
conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY,
false);
+
decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf));
+ assertFalse(decompressor.canDecompress(output, input),
+ "Block reader must fall back to stream decompression when the config
flag "
+ + "disables ByteBuff decompression");
+
+
conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY,
true);
+
decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf));
+ assertTrue(decompressor.canDecompress(output, input),
+ "Block reader must use ByteBuff decompression when the config flag is
enabled");
+
+ // The default, with no config value set, must also allow ByteBuff
decompression.
+ decompressor
+ .reinit(GzipHFileDecompressionContext.fromConfiguration(new
Configuration(false)));
+ assertTrue(decompressor.canDecompress(output, input));
+ }
+ }
+
+ @Test
+ public void itReinitWithNullContextIsNoOp() {
+ assumeNativeZlibLoaded();
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(true)) {
+ ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64));
+ ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD);
+
+ Configuration conf = new Configuration(false);
+
conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY,
false);
+
decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf));
+ assertFalse(decompressor.canDecompress(output, input));
+
+ decompressor.reinit(null);
+ assertFalse(decompressor.canDecompress(output, input),
+ "reinit(null) must not reset allowByteBuffDecompression back to the
default");
+ }
+ }
+
+ @Test
+ public void itReinitFailsOnWrongContextType() {
+ Compression.HFileDecompressionContext wrongContext =
+ new Compression.HFileDecompressionContext() {
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public long heapSize() {
+ return 0;
+ }
+ };
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(false)) {
+ decompressor.reinit(wrongContext);
+ fail("Expected an IllegalArgumentException because the context was not a
"
+ + "GzipHFileDecompressionContext");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("GzipHFileDecompressionContext"));
+ }
+ }
+
+ @Test
+ public void itDecompressThrowsWhenPassedAMultiByteBuff() throws IOException {
+ try (GzipByteBuffDecompressor decompressor = new
GzipByteBuffDecompressor(false)) {
+ ByteBuff multiOutput = new MultiByteBuff(ByteBuffer.allocate(64),
ByteBuffer.allocate(64));
+ ByteBuff input = heapBuffWith(COMPRESSED_PAYLOAD);
+ decompressor.decompress(multiOutput, input, COMPRESSED_PAYLOAD.length);
+ fail("Expected an IllegalStateException when output is a MultiByteBuff");
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("not a SingleByteBuff"));
+ }
+ }
+
+}
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/compress/TestHFileCompressionGzip.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/compress/TestHFileCompressionGzip.java
new file mode 100644
index 00000000000..8bcb0e5361c
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/compress/TestHFileCompressionGzip.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hadoop.hbase.io.compress;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.testclassification.IOTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestHFileCompressionGzip extends HFileTestBase {
+
+ private static Configuration conf;
+
+ @BeforeAll
+ public static void setUpBeforeClass() throws Exception {
+ HFileTestBase.setUpBeforeClass();
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ conf = TEST_UTIL.getConfiguration();
+ HFileTestBase.setUpBeforeClass();
+ }
+
+ @Test
+ public void testWithStreamDecompression() throws Exception {
+ conf.setBoolean("hbase.io.compress.gz.allowByteBuffDecompression", false);
+ Compression.Algorithm.GZ.reload(conf);
+
+ Path path = new Path(TEST_UTIL.getDataTestDir(),
+ HBaseTestingUtility.getRandomUUID().toString() + ".hfile");
+ doTest(conf, path, Compression.Algorithm.GZ);
+ }
+
+ @Test
+ public void testWithByteBuffDecompression() throws Exception {
+ conf.setBoolean("hbase.io.compress.gz.allowByteBuffDecompression", true);
+ Compression.Algorithm.GZ.reload(conf);
+
+ Path path = new Path(TEST_UTIL.getDataTestDir(),
+ HBaseTestingUtility.getRandomUUID().toString() + ".hfile");
+ doTest(conf, path, Compression.Algorithm.GZ);
+ }
+
+}