Skip to content

[BugFix] Distinguish bundled segment slices in page/segment caches (backport #72129)#72345

Merged
wanpengfei-git merged 1 commit into
branch-4.1from
mergify/bp/branch-4.1/pr-72129
Apr 30, 2026
Merged

[BugFix] Distinguish bundled segment slices in page/segment caches (backport #72129)#72345
wanpengfei-git merged 1 commit into
branch-4.1from
mergify/bp/branch-4.1/pr-72129

Conversation

@mergify

@mergify mergify Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Why I'm doing:

Multiple caches in shared-data key on (filename, offset) or filename
without a slice disambiguator. When a physical .dat file is written in
bundle format (a single file holding per-tablet slices at different
offsets), two slices of the same file can therefore collide on the same
cache key, and a lookup for slice B returns slice A's cached bytes —
slice A silently duplicates in query results, slice B vanishes.

The data-loss path: storage page cache

be/src/storage/rowset/page_io.cpp::read_and_decompress_page keyed the
page cache on encode_cache_key(read_file->filename(), page_pointer.offset).
For a BundleSeekableInputStream, page_pointer.offset is
stream-relative (0-based within the slice), so two slices of the same
bundled file produce identical cache keys at equal intra-slice positions.

Reproducer on a shared-data primary-key range-distributed table:

INSERT  0..20000     salt=0
UPSERT  5000..15000  salt=1
ALTER TABLE ... SPLIT TABLETS (...)                          -- four children
UPSERT  10000..25000 salt=2     -- two batches straddle child boundaries
                                -- → written as bundled files
ALTER TABLE ... MERGE TABLETS (...)                          -- one tablet again

After the merge, SELECT COUNT(DISTINCT id) returns 22,998 instead of
25,000. 1,998 ids appear twice (slice-A rows read twice), 2,002 ids
disappear entirely (slice-B rows replaced by slice-A's cached pages). The
missing-id ranges are exactly the offset-1 slice contents of the two
bundled segments.

Runtime verification: setting disable_storage_page_cache=true on all CNs
and repeating the same flow returns 25000/25000/25000 — confirming the
page cache is the sole source of the corruption.

Two adjacent defense-in-depth fixes (same mechanism)

  1. Lake Segment metacache. TabletManager::load_segment keyed on
    segment_info.path alone. Two rowsets of a bundled file in one tablet
    would share a Segment object whose _segment_file_info was bound to
    whichever slice loaded first. In isolation this does not cause the
    symptom above — the page cache collision is what corrupts reads — but
    it is the same fragility, and any future read path that bypasses the
    page cache would surface the bug.
  2. Segment cache-size refresh. Segment::update_cache_size and
    turn_off_batch_update_cache_size still passed file_name() (path
    only) to update_segment_cache_size after the Segment-cache key was
    forked. For segments with non-zero bundle_file_offset the lookup in
    cache_segment_if_present would miss, leaving the cache entry's
    memory cost unrefreshed.

Audit of other (filename, offset)-style caches

Every adjacent cache was reviewed for the same class of bug; none are
affected:

Cache File Why it is safe
Parquet page cache formats/parquet/page_reader.cpp External-table only; parquet writers do not use BundleWritableFile.
HDFS/Iceberg CacheInputStream block cache io/cache_input_stream.cpp External-table only; lake-native never wraps a bundle stream in this layer; key already includes modification_time.
SST block cache storage/sstable/table.cpp (cache_id, handle.offset) with per-Table cache_id from block_cache->new_id(); SST files are not written via BundleWritableFile.
Delvec cache lake/meta_file.cpp::delvec_cache_key (tablet_id, DelvecPagePB); delvec files are not bundled; DelvecPagePB embeds version + offset + size.
Legacy rowset metadata cache rowset/metadata_cache.cpp Keyed by rowset_id_str; no file offset.
Tablet metadata cache lake/tablet_manager.cpp Keyed by tablet_metadata_location(tablet_id, version).

The only writer that produces bundles is BundleWritableFileContext, wired
only into DeltaWriter / GeneralTabletWriter / PkTabletWriter (data
segments); PkTabletSSTWriter uses fs->new_writable_file directly, so
sstables are never bundled.

What I'm doing:

Encapsulate the slice disambiguator on the types that carry it, instead
of leaving each cache call site to know about BundleFile. Three caches
share one principle: the cache key must name the page/segment's
position in the physical file, not in the slice.

1. Page cache — be/src/storage/rowset/page_io.cpp

  • New virtual io::SeekableInputStream::page_cache_key(int64_t stream_offset).
    Default encodes (filename, stream_offset) (the existing binary format,
    moved out of page_io.cpp).
  • BundleSeekableInputStream overrides it to fold its slice base offset
    into the encoding, returning a key unique to the page's absolute byte
    position in the physical file.
  • SeekableInputStreamWrapper forwards to _impl; all wrappers
    (CacheInputStream, CountedSeekableInputStream,
    ThrottledSeekableInputStream, EncryptSeekableInputStream) inherit
    it automatically.
  • read_and_decompress_page calls
    read_file->page_cache_key(opts.page_pointer.offset) and never names
    bundle_file_offset.

2. Lake Segment metacache — be/src/fs/fs.h, be/src/storage/lake/tablet_manager.cpp

  • New FileInfo::cache_key() returns "<path>#<offset>" for bundled
    slices (bundle_file_offset > 0) and the bare path otherwise.
    Non-bundled layout unchanged.
  • TabletManager::load_segment keys both lookup_segment and
    cache_segment_if_absent on segment_info.cache_key().

3. Segment cache-size refresh — be/src/storage/rowset/segment.cpp

  • Segment::update_cache_size and turn_off_batch_update_cache_size
    route through _segment_file_info.cache_key(), so the insert key (set
    by load_segment) and the update-size key stay identical for bundled
    slices by construction.

The previous public SeekableInputStream::bundle_file_offset() accessor
and the static TabletManager::segment_cache_key() helper are removed;
the BundleFile detail only exists inside
BundleSeekableInputStream::page_cache_key and FileInfo::cache_key.
Future caches keyed on stream/file identity get correct bundled-slice
handling without any new conditionals.

Tests

New unit tests:

  • PageIOTest.test_bundle_slices_do_not_collide_in_page_cache writes two
    distinct pages back-to-back into one physical file, wraps each with its
    own BundleSeekableInputStream at offsets 0 and slice_a_size, warms
    the cache on slice A, then reads slice B at the same stream-relative
    offset 0 and asserts (1) cache miss and (2) the decoded bytes are slice
    B's, not A's.
  • LakeTabletManagerTest.segment_cache_key_distinguishes_bundle_slices
    covers the FileInfo::cache_key() rule directly.
  • LakeTabletManagerTest.load_segment_returns_distinct_slices_for_same_path
    seeds the metacache at one offset and confirms the lookup at another
    does not collide.

Existing PageIOTest and LakeTabletManagerTest suites remain green
(6/6 and 26/26 respectively).

End-to-end verification on a shared-data test cluster with the reproducer
above: the post-merge SELECT COUNT(DISTINCT id) returns 25,000 with all
expected per-id values, where the same recipe previously returned 22,998
with mixed slice-A / slice-B corruption.

Fixes #64986

What type of PR is this:

  • BugFix
  • Feature
  • Enhancement
  • Refactor
  • UT
  • Doc
  • Tool

Does this PR entail a change in behavior?

  • Yes, this PR will result in a change in behavior.
  • No, this PR will not result in a change in behavior.

Checklist:

  • I have added test cases for my bug fix or my new feature
  • This pr needs user documentation (for new or modified features or behaviors)
    • I have added documentation for my new feature or new function
    • This pr needs auto generate documentation
  • This is a backport pr

Bugfix cherry-pick branch check:

@mergify mergify Bot added the conflicts label Apr 29, 2026
@mergify

mergify Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

Cherry-pick of ab6ea26 has failed:

On branch mergify/bp/branch-4.1/pr-72129
Your branch is up to date with 'origin/branch-4.1'.

You are currently cherry-picking commit ab6ea26cad.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   be/src/fs/bundle_file.h
	modified:   be/src/fs/fs.cpp
	modified:   be/src/fs/fs.h
	modified:   be/src/io/seekable_input_stream.cpp
	modified:   be/src/io/seekable_input_stream.h
	modified:   be/src/storage/lake/tablet_manager.cpp
	modified:   be/src/storage/rowset/page_io.cpp
	modified:   be/src/storage/rowset/segment.cpp
	modified:   be/test/storage/lake/tablet_manager_test.cpp
	modified:   be/test/storage/rowset/page_io_test.cpp

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   be/src/fs/bundle_file.cpp

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

@wanpengfei-git wanpengfei-git enabled auto-merge (squash) April 29, 2026 12:22
@mergify mergify Bot closed this Apr 29, 2026
auto-merge was automatically disabled April 29, 2026 12:22

Pull request was closed

@mergify

mergify Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

@mergify[bot]: Backport conflict, please reslove the conflict and resubmit the pr

@xiangguangyxg xiangguangyxg reopened this Apr 30, 2026
@wanpengfei-git wanpengfei-git enabled auto-merge (squash) April 30, 2026 01:32
…72129)

Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit ab6ea26)
Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com>

# Conflicts:
#	be/src/fs/bundle_file.cpp
@xiangguangyxg xiangguangyxg force-pushed the mergify/bp/branch-4.1/pr-72129 branch from b9b95e7 to fb04c8c Compare April 30, 2026 01:55
@wanpengfei-git wanpengfei-git merged commit 0e943d5 into branch-4.1 Apr 30, 2026
31 checks passed
@wanpengfei-git wanpengfei-git deleted the mergify/bp/branch-4.1/pr-72129 branch April 30, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants