Skip to content

Add bounded codec runtime and compression handlers - #19285

Open
xiangfu0 wants to merge 3 commits into
xiangfu0/codex/codec-stack/01-configfrom
xiangfu0/codex/codec-stack/02-runtime
Open

Add bounded codec runtime and compression handlers#19285
xiangfu0 wants to merge 3 commits into
xiangfu0/codex/codec-stack/01-configfrom
xiangfu0/codex/codec-stack/02-runtime

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Context

This is the second layer extracted from #18229 and depends on #19284 through GitHub's native stacked-PR feature.

What changed

  • Adds a closed codec runtime under pinot-segment-local; it does not publish a pluggable codec SPI.
  • Adds LZ4, Snappy, Gzip, and Zstd handlers, including canonical ZSTD(level) options and the legacy ZSTANDARD input alias.
  • Adds wrapperless multi-stage execution with forward encode and reverse decode order.
  • Bounds encoded size, each intermediate allocation, cumulative work, outer input size, and exact final decoded size.
  • Cleans direct scratch buffers on success and failure and validates corrupt/truncated/trailing frames.
  • Keeps transform ordering generic so concrete transforms can be reviewed in later stack layers.

Safety and scope

Only CodecPipelineExecutor is public across local packages; registry, handlers, options, context, and validator remain package-private. The only public decode entry point requires explicit decoded-size, per-stage, and cumulative bounds.

This PR changes no forward-index creator, reader, writer, handler, config, or on-disk format. All fail-closed gates from #19284 remain in place, so codecSpec is still not usable after this layer alone.

Tests

  • 49 final runtime, round-trip, ordering, bound, and corrupt-input tests
  • 13 unchanged fail-closed gate regressions from Add codec spec DSL and configuration plumbing #19284
  • Spotless, Checkstyle, license format, and license check for pinot-segment-local
  • final git diff --check and stack range-diff are clean

@xiangfu0 xiangfu0 added feature New functionality index Related to indexing (general) labels Aug 18, 2026
@xiangfu0
xiangfu0 force-pushed the xiangfu0/codex/codec-stack/02-runtime branch from 9adf1cc to db04987 Compare August 18, 2026 02:46
@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.67343% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.22%. Comparing base (613c304) to head (d4f9863).

Files with missing lines Patch % Lines
...ot/segment/local/io/codec/GzipCodecDefinition.java 65.45% 28 Missing and 10 partials ⚠️
...ot/segment/local/io/codec/ZstdCodecDefinition.java 64.70% 20 Missing and 10 partials ⚠️
.../segment/local/io/codec/CodecPipelineExecutor.java 77.68% 15 Missing and 12 partials ⚠️
...not/segment/local/io/codec/Lz4CodecDefinition.java 79.62% 6 Missing and 5 partials ⚠️
.../segment/local/io/codec/SnappyCodecDefinition.java 85.18% 4 Missing and 4 partials ⚠️
...che/pinot/segment/local/io/codec/CodecContext.java 75.00% 1 Missing ⚠️
Additional details and impacted files
@@                             Coverage Diff                             @@
##             xiangfu0/codex/codec-stack/01-config   #19285       +/-   ##
===========================================================================
+ Coverage                                   39.17%   67.22%   +28.04%     
- Complexity                                   1423     1424        +1     
===========================================================================
  Files                                        3465     3476       +11     
  Lines                                      220562   221055      +493     
  Branches                                    35201    35285       +84     
===========================================================================
+ Hits                                        86409   148596    +62187     
+ Misses                                     126278    60594    -65684     
- Partials                                     7875    11865     +3990     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.22% <76.67%> (+28.04%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.22% <76.67%> (+28.04%) ⬆️
unittests 67.21% <76.67%> (+28.04%) ⬆️
unittests1 57.67% <0.00%> (?)
unittests2 39.27% <76.67%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0
xiangfu0 force-pushed the xiangfu0/codex/codec-stack/02-runtime branch from db04987 to 8088771 Compare August 18, 2026 07:22
@xiangfu0
xiangfu0 marked this pull request as ready for review August 18, 2026 07:23
@xiangfu0
xiangfu0 requested review from Jackie-Jiang and a balanced review from Copilot August 18, 2026 07:23
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Two notes from review:

  1. SnappyCodecDefinition.decode allocates a direct buffer from the untrusted header-declared decompressed size with only a < 0 check — LZ4/GZIP/ZSTD all apply a sanity cap. Latent today (the path is package-private and unused in production), but the V7 reader PR later in this stack makes it reachable from segment data. I'll include the cap plus a standalone Snappy corrupt-input test in that PR unless you'd rather fix it here.

  2. Cross-PR invariant for the rest of the stack: ForwardIndexConfig stores the structural normalization (toDslString), which can differ from the executor's getCanonicalSpec() (e.g. ZSTANDARDZSTD(3)). The V7 header write and the reload spec comparison must canonicalize via the executor on both sides — never persist or compare the config-stored string.

Copilot AI left a comment

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.

Pull request overview

Adds a closed, bounded codec runtime to pinot-segment-local, building on the codec DSL while remaining disconnected from index formats.

Changes:

  • Adds LZ4, Snappy, Gzip, and Zstd handlers.
  • Adds pipeline validation, bounded execution, and buffer cleanup.
  • Adds round-trip, corruption, ordering, visibility, and bounds tests.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ZstdCodecDefinitionTest.java Tests empty Zstd frames.
CompressionCodecCorruptInputTest.java Tests corrupt inputs and bounds.
CodecRegistryTest.java Tests registry closure and aliases.
CodecPipelineValidatorTest.java Tests pipeline ordering rules.
CodecPipelineExecutorTest.java Tests execution, bounds, and visibility.
ZstdCodecDefinition.java Implements bounded Zstd handling.
SnappyCodecDefinition.java Implements Snappy handling.
Lz4CodecDefinition.java Implements length-prefixed LZ4 handling.
GzipCodecDefinition.java Implements framed Deflate/Gzip handling.
CodecRegistry.java Defines the closed codec registry.
CodecPipelineValidator.java Validates stage ordering and context.
CodecPipelineExecutor.java Executes bounded codec pipelines.
CodecOptions.java Defines codec option contracts.
CodecKind.java Classifies pipeline stages.
CodecDefinition.java Defines codec metadata contracts.
CodecContext.java Carries stored-type context.
CodecBufferUtils.java Provides direct-buffer utilities.
ChunkCodecHandler.java Defines codec execution operations.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

if (payloadLimit < Integer.BYTES) {
throw new IOException("GZIP payload too short to contain uncompressed-size footer: " + payloadLimit + " bytes");
}
int decompressedSize = src.getInt(payloadLimit - Integer.BYTES);
Comment on lines +208 to +210
ByteBuffer compressed = src.duplicate();
compressed.position(0);
compressed.limit(src.limit() - Integer.BYTES);
Comment on lines +317 to +318
output = i == 0 ? dst : ByteBuffer.allocateDirect(maxOutputAfterStage[i - 1]);
_stages.get(i).decodeInto(current, output);
…TD(0)

- Snappy was the only pipeline codec missing the sanity cap on the
  header-declared decompressed size; a corrupt or hostile header must not
  drive allocation. Rather than adding a fourth private copy of the cap,
  hoist it into CodecBufferUtils.checkDeclaredDecompressedSize and route
  all four codecs' guard sites through it, so the bound cannot be omitted
  from a future codec or silently drift between implementations. Corrupt-
  input tests cover both Snappy decode paths at the cap boundary (cap + 1).
- CodecRegistry.register() rejects the reserved wrapper name via the shared
  CodecSpecParser.REMOVED_WRAPPER_NAME constant as defense in depth
  alongside the parser and CodecInvocation reservations.
- ZSTD(0) is rejected: zstd treats level 0 as "use the default level", so
  it would behave identically to ZSTD(3) under a second canonical spelling,
  and the canonical spec is frozen into segment headers. Rejection is
  reversible later; acceptance is not.
@xiangfu0
xiangfu0 force-pushed the xiangfu0/codex/codec-stack/02-runtime branch from 8088771 to bfe108c Compare August 19, 2026 00:49
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Pushed `bfe108c3d9` (rebased onto the updated #19284 head) addressing review findings on the runtime.

Snappy decode size cap, fixed as a pattern rather than a fourth copy. `SnappyCodecDefinition.decode` allocated from the header-declared decompressed size with only a negative check, while LZ4/GZIP/ZSTD each carried a 1 GiB sanity cap. That gap is exactly the drift a per-codec hand-rolled constant produces, so instead of adding a fourth private copy I hoisted it into `CodecBufferUtils.checkDeclaredDecompressedSize(declared, codec, source)` and routed all four codecs through it. A future codec cannot omit the bound, and the policy is single-sourced. New corrupt-input tests cover both Snappy paths — including `decodeInto`, which is the one the bounded executor actually uses in production — at the boundary (cap + 1), plus a garbage-input test Snappy was missing.

`CodecRegistry.register()` rejects the reserved name `CODEC` as defense in depth alongside the parser and `CodecInvocation` reservations, referencing `CodecSpecParser.REMOVED_WRAPPER_NAME` (made public in #19284) rather than re-spelling the literal.

`ZSTD(0)` is now rejected; the accepted range is `[1, maxCompressionLevel]`. zstd treats level 0 as "use the default level", so `ZSTD(0)` and `ZSTD(3)` would be identical behavior under two canonical spellings — the same one-spelling-per-behavior rule that motivates the leading-zero fix, and worth settling before the header format freezes. Use bare `ZSTD` for the default.

One follow-up worth filing separately (deliberately not fixed here, since it is a different long-shipped subsystem): the legacy `SnappyDecompressor.decompressedLength()` in `io/compression` has the same unbounded-header shape, and shipped callers such as `VarByteChunkForwardIndexReaderV4` allocate from it.

Verification: 53 codec tests plus 177 downstream gate/validation/handler tests green (`CodecPipelineExecutorTest`, `CodecPipelineValidatorTest`, `CodecRegistryTest`, `CompressionCodecCorruptInputTest`, `ZstdCodecDefinitionTest`, `ForwardIndexTypeTest`, `TableConfigUtilsTest`, `ForwardIndexCreatorFactoryTest`, `OpenStructIndexTypeTest`, `ForwardIndexHandlerTest`); spotless/checkstyle/license clean.

@xiangfu0

Copy link
Copy Markdown
Contributor Author

Note: pushed a master sync to this branch (merge commit, no code changes of its own) so the downstream stack includes the #19282 chunk-caching fix that the reload tests in #19308 depend on.

The full stack continuing this PR: #19305 (DELTA/DELTADELTA) → #19306 (T64/GORILLA) → #19307 (V7 format) → #19308 (reload + enable) → #19309 (integration tests + docs). Each is based on its predecessor's branch.

@xiangfu0 xiangfu0 added serialization Related to data serialization and deserialization memory Related to memory usage or optimization labels Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New functionality index Related to indexing (general) memory Related to memory usage or optimization serialization Related to data serialization and deserialization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants