test: pin Snappy codec reuse contract ahead of #2352; document upstream allocation blocker - #2524
test: pin Snappy codec reuse contract ahead of #2352; document upstream allocation blocker#2524thomhurst wants to merge 2 commits into
Conversation
…am allocation blocker Issue #2214's two work items resolved to different fates: the DetachableBufferWriter pooling in PreCompress already shipped on main via PR #2416, and Snappier compressor/decompressor instance reuse is blocked upstream - Snappier 1.3.1 keeps SnappyCompressor/SnappyDecompressor internal and every static entry point allocates a fresh driver per call (~48 B compress / ~80 B decompress). Issue #2352 rules out reflection and vendoring (Native AOT + trimming), so the swap waits on an upstream release. What this change adds while blocked: - A source comment in SnappyCompressionCodec recording the allocation cause and its per-block (per-batch, not per-message) classification, so a future zero-allocation audit does not misdiagnose or wrongly 'fix' it. - Contract-pinning tests the eventual #2352 swap must keep green: reused codec produces byte-identical output to a fresh instance across full compress/decompress cycles, and 8 parallel workers sharing one codec round-trip without cross-thread corruption (single- and multi-segment). - A nested-Rent independence test for DetachableBufferWriter's documented single-slot wrapper cache (guards PR #2416's invariant). No behavioral src change (comment only) - no benchmark numbers required.
Greptile SummaryThe PR documents the upstream blocker preventing reusable Snappy drivers and adds contract tests for codec reuse, parallel isolation, and nested buffer-writer rentals.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported aligned-split coverage gap is fixed by placing the segment boundary inside a compression block.
|
| Filename | Overview |
|---|---|
| src/Dekaf.Compression.Snappy/SnappyCompressionCodec.cs | Adds a documentation-only note explaining the remaining per-block allocations and upstream reuse blocker. |
| tests/Dekaf.Tests.Unit/Compression/SnappyCompressionCodecTests.cs | Adds codec-reuse and parallel-isolation tests; the revised off-boundary split now exercises the intended multi-segment scratch-buffer path. |
| tests/Dekaf.Tests.Unit/Protocol/DetachableBufferWriterTests.cs | Adds coverage confirming that a nested rental receives an independent writer while another rental is outstanding. |
Reviews (2): Last reviewed commit: "test: straddle block boundary in Snappy ..." | Re-trigger Greptile
There was a problem hiding this comment.
Review Summary
Reviewed against CLAUDE.md's rules, in particular the zero-allocation hot-path gate (Rule 1) and the no-benchmarks-needed-for-non-behavioral-changes carve-out. This PR is test/comment-only — verified the src/ diff is a 6-line comment addition to SnappyCompressionCodec with no logic change, so the "no behavioral change, no benchmark numbers required" claim in the PR description checks out.
Verification performed:
- Built
tests/Dekaf.Tests.Unitin Release from the PR branch — 0 warnings, 0 errors. - Ran the affected test classes (
SnappyCompressionCodecTests,DetachableBufferWriterTests) — all 23 tests pass, including the 3 new ones, in 608ms (no flakiness observed). - Read
DetachableBufferWriter(src/Dekaf/Protocol/Records/RecordBatch.cs) to confirm the newRent_WhileOutstanding_ReturnsIndependentWritertest actually exercises the documented single-slot[ThreadStatic]cache invariant it claims to pin — it does: two back-to-backRentcalls without an interveningDisposeare guaranteed to return distinct instances regardless of what was left in the thread-local cache slot by a prior test, so the assertion holds independent of test execution order. - Read the
SnappyCompressionCodecthread-local scratch fields (t_compressedBuffer,t_decompressionDestination) to confirm the new reuse/parallel tests target real state that could leak across calls — they do, and the tests correctly avoid throwing insideParallel.Forworkers by aggregating failures viaInterlocked.Incrementand asserting once after the loop. - No prior review comments/reviews existed on this PR to reconcile against.
Findings: None blocking. One trivial nit, not worth blocking on:
SnappyCompressionCodecTests.cs: inCompress_ReusedCodec_ProducesIdenticalOutputToFreshInstance,reusedOutputis initialized once before theforloop and then unconditionally reassigned as the first statement inside the loop — the pre-loop initialization is dead. Harmless (test-only, not a hot path Rule 1 concerns), butArrayBufferWriter<byte> reusedOutput = null!;before the loop would avoid the redundant allocation-then-discard.
Design/architecture: The comment correctly classifies the Snappier allocation as per-block (≈per-batch given default 1 MiB block size aligning with the project's ~1 MiB default batch size), consistent with the project's per-message-vs-per-batch cost framing in CLAUDE.md. The new tests are well-targeted contract-pinning tests for the eventual #2352 swap rather than incidental coverage, and correctly avoid asserting anything about the (currently non-zero, upstream-blocked) allocation counts themselves.
Clean, well-scoped PR. No correctness, security, or coverage issues found.
test: straddle block boundary in Snappy reuse contract test Review (#2524): the reuse test split its 8192-byte payload at 4096 with 1024-byte blocks, so every block sat inside one segment and Compress always took the IsSingleSegment fast path -- the thread-cached multi-segment scratch buffer the test claims to pin was never touched. Split at 4097 so the fifth block straddles both segments. Also drops the dead pre-loop ArrayBufferWriter initialisation that was unconditionally reassigned on the first iteration. @
Review (#2524): the reuse test split its 8192-byte payload at 4096 with 1024-byte blocks, so every block sat inside one segment and Compress always took the IsSingleSegment fast path -- the thread-cached multi-segment scratch buffer the test claims to pin was never touched. Split at 4097 so the fifth block straddles both segments. Also drops the dead pre-loop ArrayBufferWriter initialisation that was unconditionally reassigned on the first iteration.
ed17b4e to
d697d5f
Compare
There was a problem hiding this comment.
Review Summary
Test/comment-only PR (per CLAUDE.md Rule 1's no-behavioral-change carve-out — verified the src/ diff is a 6-line comment addition to SnappyCompressionCodec with zero logic change, so the "no benchmark numbers required" claim holds).
Verification performed:
- Built
tests/Dekaf.Tests.Unitin Release — 0 warnings, 0 errors. - Ran
SnappyCompressionCodecTests+DetachableBufferWriterTests— 23/23 pass in 698ms. - Hand-traced the
Compress_ReusedCodec_ProducesIdenticalOutputToFreshInstanceblock/segment math: withblockSize=1024and the newstraddlingSplit = payload.Length/2 + 1 = 4097, segment 1 covers bytes[0,4097)and segment 2 covers[4097,8192). Block 4 ([4096,5120)) starts inside segment 1 and ends inside segment 2, soblockSequence.IsSingleSegmentis false for that block andCompresstakes thet_compressedBuffermulti-segment path the test exists to pin. All other blocks are fully within one segment. This confirms the fix is correct, not just plausible. - Confirmed against prior review history: this PR already incorporates fixes for both issues raised in earlier review rounds on this same PR —
- Greptile's finding that the original 4096-byte aligned split let every block sit inside one segment, so the reuse test never touched the multi-segment scratch buffer it claimed to cover — fixed by the
d697d5fcommit's 4097 split (verified above). - My own prior nit about the dead pre-loop
ArrayBufferWriterinitialization in the same test — the same commit removed it (ArrayBufferWriter<byte> reusedOutput = null!;declared once, assigned only inside the loop).
- Greptile's finding that the original 4096-byte aligned split let every block sit inside one segment, so the reuse test never touched the multi-segment scratch buffer it claimed to cover — fixed by the
- Read
DetachableBufferWriter(src/Dekaf/Protocol/Records/RecordBatch.cs:189-273) to confirmRent_WhileOutstanding_ReturnsIndependentWriteractually exercises the single-slot[ThreadStatic]cache:t_cachedWriteris only repopulated inDispose(), so a nestedRentbefore the outer writer is disposed is guaranteed to allocate a distinct instance — the test's assertion holds regardless of execution order or prior test state. CompressDecompress_ParallelWorkers_DoNotCorruptEachOthercorrectly aggregates failures viaInterlocked.Incrementinstead of asserting insideParallel.Forworkers (avoids the anti-pattern of assertions on non-main threads being silently swallowed).
Findings: None blocking, no new issues found beyond what earlier review rounds already caught and this PR already fixed.
Design/architecture: The allocation classification in the new source comment (per-block ≈ per-batch given the 1 MiB default block size aligning with the project's ~1 MiB default batch size) is accurate per CLAUDE.md's per-message-vs-per-batch cost framing, and correctly avoids the trap of "fixing" a bounded, upstream-blocked allocation the forbidden way. The new tests are targeted contract-pinning tests for the eventual #2352 swap, not incidental coverage, and none of them assert on the (currently non-zero) allocation counts themselves, so they won't spuriously fail once #2352 lands.
Clean, well-scoped PR that has fully addressed prior review feedback. No correctness, security, or coverage issues found.
|
Review feedback addressed in ed17b4e. greptile P2 — aligned split skips the scratch path: real, fixed. With an 8192-byte payload in 1024-byte blocks and the segment boundary at 4096, every block sat inside one segment, so Claude review nit — dropped the dead pre-loop
|
Summary
Investigated #2214 ("last zero-alloc leftovers") to implementation. Findings changed the shape of the work:
DetachableBufferWriterinPreCompressRent+[ThreadStatic]single-slot wrapper cache; producer pre-serialization measured 40 B → 0 B there. Nothing left to do.SnappyCompressor/SnappyDecompressorinternal; every public static entry point allocates a fresh driver per call (~48 B compress / ~80 B decompress). #2352 rules out reflection and vendoring (Native AOT + trimming). Upstream brantburnett/Snappier#148 is still open — the swap needs it merged and released, then a package bump.So no product perf change is possible right now. This PR lands what is useful while blocked:
SnappyCompressionCodecclassifying the residual allocations as per-block (per-batch) costs with the upstream blocker, so future zero-alloc audits don't misdiagnose them or "fix" them the forbidden way.Compress_ReusedCodec_ProducesIdenticalOutputToFreshInstance— thread-cached scratch never leaks across compress/decompress cycles.CompressDecompress_ParallelWorkers_DoNotCorruptEachOther— 8 workers × 50 batches through one shared codec, single- and multi-segment inputs; catches ownership bugs if thread-local caches are ever converted to a shared pool incorrectly.Rent_WhileOutstanding_ReturnsIndependentWriter— pinsDetachableBufferWriter's documented single-slot cache invariant from Cache detachable record-batch writers #2416.Reviewed via /simplify (4 agents): segment-building now delegates to the shared
SequenceTestHelpers, writers pre-sized, comment trimmed to durable facts. Efficiency review specifically verified the parallel test cannot perturb the pool-warmth-sensitiveTransactionalProduceAllocationTestsgate (NotInParallel + disjoint pool buckets + its windowed design).Test plan
dotnet build Dekaf.sln -c Release -p:TreatWarningsAsErrors=true— 0 warnings/errorsNo behavioral
src/change (comment only) — no benchmark numbers required. Suggest keeping #2214 open (or re-scoping it onto #2352) until upstream ships.