Skip to content

Implement GzipByteBuffDecompressor with on-heap and off-heap decompression paths - #8541

Draft
SaadASTheDev wants to merge 1 commit into
apache:masterfrom
HubSpot:HBASE-30321-gzipbytebuff
Draft

Implement GzipByteBuffDecompressor with on-heap and off-heap decompression paths#8541
SaadASTheDev wants to merge 1 commit into
apache:masterfrom
HubSpot:HBASE-30321-gzipbytebuff

Conversation

@SaadASTheDev

Copy link
Copy Markdown
Contributor

Why

HBase's ByteBuffDecompressor interface enables block decompression directly from/to
ByteBuffer objects, avoiding intermediate byte array copies. GZIP had no implementation
of this interface — all GZIP decompression went through a byte array round-trip regardless
of whether the underlying buffers were on-heap or off-heap.

What

Adds GzipByteBuffDecompressor, a ByteBuffDecompressor for GZIP that handles two cases:

  • Off-heap (direct ByteBuffers): delegates to Hadoop's native ZlibDirectDecompressor
    with GZIP_FORMAT. Requires native zlib to be loaded; canDecompress() returns false
    if it isn't.
  • On-heap (heap ByteBuffers): uses Java's Inflater in raw DEFLATE (nowrap) mode,
    skipping the 10-byte GZIP header manually and verifying the CRC32 and ISIZE trailer
    fields after inflation. The native ZlibDirectDecompressor requires a stable native
    memory address, which heap ByteBuffers do not provide, making this fallback necessary.

Also adds:

  • GzipHFileDecompressionContext — carries per-context configuration (e.g. whether
    ByteBuff decompression is allowed), wired in via reinit()
  • Input-length validation — rejects inputs shorter than the minimum valid GZIP member
    (header + trailer = 18 bytes)

Testing

  • Off-heap → off-heap decompression
  • On-heap → on-heap decompression
  • CRC32 mismatch detection
  • ISIZE mismatch detection
  • Input too short
  • Output buffer too small
  • canDecompress() guard logic

private static final int GZIP_HEADER_LENGTH = 10;
private static final int GZIP_TRAILER_LENGTH = 8;

@Nullable

@SaadASTheDev SaadASTheDev Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the Singleton objects reused across decompress calls, Allocated once per buffer, to avoid GC overhead

allowByteBuffDecompression = true;
}

@Override

@SaadASTheDev SaadASTheDev Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flow for wether we can decompress is
1: verify config via context to ensure we havent disabled this feature before we try to decompress via bytebuff
2. we only support singleByteBuff decompression for now
3. Since Zlib is a JNI we need to ensure that its properly instantiated if and only if the buffers we are trying to decompress are direct, so if both are direct we ensure the decompressor is loaded
4. then we return if both I/O are not direct, if true it uses the inflater pathway

}
return decompressOnHeap(nioInput, nioOutput, inputLen);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Respective off heap decompress which uses the zlib library and verifies the trailer on its own

}

@Override
public void reinit(@Nullable Compression.HFileDecompressionContext newHFileDecompressionContext) {

@SaadASTheDev SaadASTheDev Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We load in a new context at every new Hfile, it would be too expensive to do it at every block

@SaadASTheDev
SaadASTheDev force-pushed the HBASE-30321-gzipbytebuff branch from 628789b to cedb75b Compare August 7, 2026 17:57
@Nullable
private final ZlibDecompressor.ZlibDirectDecompressor decompressor;

private final Inflater inflater = new Inflater(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, I suggest you use the top-level ZlibDecompressor to handle on-heap

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, wasn't familiar with the top level decompressor but that simplifies things, Thanks!

Comment on lines +110 to +123
while (!decompressor.finished()) {
int outputRemainingBefore = nioOutput.remaining();
try {
decompressor.decompress(gzipMember, nioOutput);
} catch (IOException e) {
throw new IOException("Invalid gzip stream: " + e.getMessage(), e);
}
if (nioOutput.remaining() == outputRemainingBefore && !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");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this needs to loop? Why would the decompressor need multiple attempts?

@SaadASTheDev SaadASTheDev Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea It doesnt need to loop. I was under the impression the zlib decompressor only follows a streaming, chunk-based pattern, where a block gets decompressed in chunks. Thats only true if we don't pre size the output buffer, going to fix this, Thanks

Comment on lines +42 to +45
/*
* "HBase is fun to use and very fast" compressed as a single gzip member via GZIPOutputStream,
* matching the framing that ReusableStreamGzipCodec produces on the compression side.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there are situations where a gzip input payload would contain multiple members?

@SaadASTheDev SaadASTheDev Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are situations, the payload will contain multiple members but we enclose over ours via: InputStart + InputLen, but we should test to make sure we did it correctly

@SaadASTheDev
SaadASTheDev force-pushed the HBASE-30321-gzipbytebuff branch from cedb75b to 1407839 Compare August 7, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants