perf(table): load deletion-vector Puffin groups lazily - #1968
perf(table): load deletion-vector Puffin groups lazily#1968fallintoplace wants to merge 2 commits into
Conversation
4e77575 to
283bfaf
Compare
zeroshade
left a comment
There was a problem hiding this comment.
The Puffin lazy-loading machinery is solid, but there's one validation gap in the new code path that can silently return deleted rows. That needs fixing before this lands.
Blocking — empty referenced_data_file silently drops a deletion vector (table/arrow_scanner.go:234-263)
collectUniqueDeletionVectors rejects a nil ref but permits an empty one:
_, _, ref, contentOffset, contentSize := iceinternal.BorrowedDataFilePointers(d)
if ref == nil {
return nil, fmt.Errorf("deletion vector %s missing referenced_data_file", d.FilePath())
}With *ref == "" this falls through to uniqueDVs[*ref] = d, indexing the DV under the empty key. The worker then looks it up by the task's actual data-file path (:1937), which is never "", so the DV is never found and the scan proceeds as though that data file had no deletion vector — returning rows that have been deleted.
What makes this blocking rather than a hardening nit is that the path it replaces already rejected this. table/dv/deletion_vector.go:339-341 on main:
if manifestReferencedDataFile == nil || *manifestReferencedDataFile == "" {
return fmt.Errorf("%w: DV file %s missing or empty %s property", ErrInvalidDeletionVector, ...)
}Note the || *... == "". So this is a regression in failure mode: an input the codebase deliberately fails closed on now silently produces wrong query results.
This reads as a simple oversight rather than a design decision — the adjacent contentOffset/contentSize check in the same function has a careful comment about surfacing the real cause instead of a misleading dedup error, so the validation intent is clearly there.
Two ways to fix it:
- Minimal:
if ref == nil || *ref == ""at:239, ideally wrappingErrInvalidDeletionVectorso the error identity matches the eager path it replaced. - Preferred: validate every DV entry up front through the existing shared validator, so the lazy and eager paths cannot drift apart again. The bug exists precisely because a second validation site was introduced with slightly weaker rules.
Please also add a regression test with a DV entry whose referenced_data_file is empty, asserting an error rather than a successful scan with deleted rows present.
Everything else checks out
To be clear about scope — the rest of the lazy DV work looks right:
- Puffin integrity is preserved. The lazy load calls the unchanged
dv.ReadDVs(arrow_scanner.go:199-220), which still goes throughpuffin.NewReaderheader/footer magic validation, footer metadata index and range checks, andDeserializeDVmagic/length/CRC/bitmap plus manifest-cardinality checks, and defersfile.Close. - Absent vs failed is distinguished.
ReadDVsreturnsnil, erroron any partial group failure, so a failed read cannot masquerade as "no deletes". A genuinely empty bitmap is non-nil and correctly means no deletes. - Concurrency. The group
sync.Onceis per Puffin path and cachesgroup.err, so every waiter observes the same error and each Puffin file is read once. Shared-file, concurrent, and cached-open-error tests cover it. - No leak on abandonment. An unread iterator opens no Puffin reader, and the open-count test pins that.
Minor — lazy-path corruption and abandonment coverage
Tests cover valid shared-file on-demand loading (lazy_deletion_vector_test.go:35-65), concurrent single-flight (:68-97), cached open errors (:113-139), and unread-iterator open count (:158-181), but none drives a corrupt or truncated Puffin through lazyDeletionVectorLoader, or abandons a live iterator mid-read. The lower-level table/dv tests cover corruption at the ReadDV/Deserialize level, so this is a gap in the new layer rather than an absence of coverage overall.
Not a finding, noted for completeness
Context is checked before and after the synchronous ReadDVs, but File has no context-aware ReadAt, so cancellation can't interrupt an in-flight read. That limitation is inherited from the eager path, not introduced here.
CI green (15/15), all 3 commits signed off.
This review was drafted by an AI-assisted tool and confirmed by an Apache Iceberg Go maintainer. The findings cite the project's review criteria; if you think one of them is mis-applied, please reply on the PR and a maintainer will weigh in.
More on how to contribute to Apache Iceberg Go: CONTRIBUTING.md
Signed-off-by: Minh Vu <vuhoangminh97@gmail.com>
283bfaf to
9b03e3e
Compare
Summary
Benchmark
Apple M1 Pro. 100 Puffin groups with 10 DV blobs per group.
Tests
go test ./table/... -count=1go test -race ./table/... -count=1go vet ./...golangci-lint run --timeout=10m