Skip to content

Commit a2753bc

Browse files
committed
[BugFix] Retain lake vacuum files by version interval across tablet reshard
Auto-vacuum stalled and could lose snapshot data on shared-data range-distribution partitions after a tablet split/merge when a cluster snapshot pinned a pre-reshard version V: - TabletRetainInfo::init read {tablet}_{V}.meta for each retained version to build its protect set. On a reshard child, a pinned pre-reshard version V predates the child's earliest metadata, so the read returned NotFound and init failed. vacuum_tablet_metadata runs init before any deletion, so the whole partition's vacuum RPC failed every cycle and expired metadata / data files / txn logs accumulated for as long as the snapshot existed. - Skipping the NotFound version, or only suppressing shared-file cleanup, is unsafe: the incremental vacuum can delete a pre-reshard file the snapshot still needs, and the reshard "identical tablet" path does not mark inherited files shared, so a shared-flag-based guard misses them. Decide retention by version interval instead of by reading the pinned version's metadata. A garbage file is kept iff it was live at a retained version, i.e. there is a retained version V with create_version <= V < remove_version, where remove_version is the metadata version that moved it into compaction_inputs / orphan_files. - TabletRetainInfo now holds only the retained-version set (no metadata reads, so the NotFound stall cannot occur) and answers retained_by_version(create, remove) and contains_version(v). - collect_garbage_files decides each garbage file's creation version: - compaction_inputs segments share the rowset's version and are retained by rowset.version(). Lake partial-segment compaction can carry older segments into a higher-versioned rowset, but it is config-gated (off by default), non-PK, and being retired in favor of parallel compaction, so it is deliberately not special-cased; - del files can be transferred by cloud-native PK compaction onto a higher-versioned rowset, so they carry a new optional DelfileWithRowsetId .version and are retained per file; - orphan files carry a new optional FileMetaPB.version, stamped at each orphaning site from the source object's own version. An unset/0 version means "unknown -> retain conservatively", never a false delete. Keyed on version rather than the per-file shared flag, this protects both shared and exclusive/identical inherited files. No user-facing SQL/config/API/metric change; adds two backward-compatible optional fields to the storage metadata proto. Range distribution is a new, new-tables-only feature with no on-disk compatibility burden. Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com>
1 parent 35c68c6 commit a2753bc

9 files changed

Lines changed: 453 additions & 261 deletions

File tree

be/src/storage/lake/meta_file.cpp

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ void MetaFileBuilder::append_dcg(uint32_t rssid,
191191
if (dcg_ver.shared_files_size() > 0 && i < dcg_ver.shared_files_size()) {
192192
file_meta.set_shared(dcg_ver.shared_files(i));
193193
}
194+
if (i < dcg_ver.versions_size()) {
195+
file_meta.set_version(dcg_ver.versions(i));
196+
}
194197
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
195198
}
196199
}
@@ -273,14 +276,24 @@ void MetaFileBuilder::apply_opwrite(const TxnLogPB_OpWrite& op_write,
273276
if (del_meta.has_shared()) {
274277
del_file_with_rid.set_shared(del_meta.shared());
275278
}
279+
// Freshly created del file: stamp its creation version so lake vacuum can retain it by its
280+
// own version after it is later transferred onto a higher-versioned compaction output rowset.
281+
del_file_with_rid.set_version(rowset->version());
276282
rowset->add_del_files()->CopyFrom(del_file_with_rid);
277283
}
278284
// if rowset don't contain segment files, still inc next_rowset_id
279285
_tablet_meta->set_next_rowset_id(_tablet_meta->next_rowset_id() + get_rowset_id_step(*rowset));
280286
// collect trash files: replaced partial-update segments and their segment-name-keyed .vi files
281287
for (const auto& orphan_file : orphan_files) {
282288
DCHECK(is_segment(orphan_file.name()) || is_vector_index(orphan_file.name()));
283-
_tablet_meta->mutable_orphan_files()->Add()->CopyFrom(orphan_file);
289+
auto* added_orphan = _tablet_meta->mutable_orphan_files()->Add();
290+
added_orphan->CopyFrom(orphan_file);
291+
// These replaced segment/.vi files are produced by this txn's partial-update rewrite, so
292+
// their creation version is the metadata version being built. Respect an accurate version if
293+
// the producer already set one.
294+
if (!added_orphan->has_version()) {
295+
added_orphan->set_version(_tablet_meta->version());
296+
}
284297
}
285298
if (!_tablet_meta->rowset_to_schema().empty()) {
286299
auto schema_id = _tablet_meta->schema().id();
@@ -301,6 +314,9 @@ void MetaFileBuilder::apply_column_mode_partial_update(const TxnLogPB_OpWrite& o
301314
// sibling tablets. The orphan FileMetaPB only carries `shared`, so encode bundling into it
302315
// too, otherwise vacuum deletes the shared bundle file while a sibling still references it.
303316
file_meta.set_shared(segment_meta.shared() || segment_meta.has_bundle_file_offset());
317+
// These segments are produced and discarded within this txn, so their creation version is
318+
// the metadata version being built.
319+
file_meta.set_version(_tablet_meta->version());
304320
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
305321
}
306322
}
@@ -502,6 +518,7 @@ void MetaFileBuilder::apply_drop_index(const TxnLogPB_OpDropIndex& op) {
502518
file_meta.set_name(entry.index_file());
503519
if (entry.has_file_size()) file_meta.set_size(entry.file_size());
504520
if (entry.has_shared_file()) file_meta.set_shared(entry.shared_file());
521+
file_meta.set_version(entry.version());
505522
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
506523
}
507524
}
@@ -636,6 +653,7 @@ void MetaFileBuilder::remove_compacted_sst(const TxnLogPB_OpCompaction& op_compa
636653
}
637654
}
638655
file_meta.set_shared(shared);
656+
file_meta.set_version(input_sstable.version());
639657
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
640658
}
641659
}
@@ -652,7 +670,12 @@ void MetaFileBuilder::remove_lcrm_file(const TxnLogPB_OpCompaction& op_compactio
652670
// tablet metadata GC process, which runs periodically and handles failures gracefully.
653671
// This approach is safe, non-blocking, and handles distributed cleanup correctly.
654672
if (op_compaction.has_lcrm_file()) {
655-
_tablet_meta->add_orphan_files()->CopyFrom(op_compaction.lcrm_file());
673+
auto* added = _tablet_meta->add_orphan_files();
674+
added->CopyFrom(op_compaction.lcrm_file());
675+
// The mapper file is produced and orphaned by this compaction and is never referenced by any
676+
// visible tablet metadata, so its creation version is the metadata version being built.
677+
// Stamping it lets vacuum reclaim it instead of over-retaining it under a covering snapshot.
678+
added->set_version(_tablet_meta->version());
656679
}
657680
}
658681

@@ -737,6 +760,9 @@ Status MetaFileBuilder::apply_opcompaction(const TxnLogPB_OpCompaction& op_compa
737760
if (dcg.shared_files_size() > 0) {
738761
file_meta.set_shared(dcg.shared_files(i));
739762
}
763+
if (i < dcg.versions_size()) {
764+
file_meta.set_version(dcg.versions(i));
765+
}
740766
// Put useless `.cols` files into orphan files
741767
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
742768
}
@@ -761,6 +787,7 @@ Status MetaFileBuilder::apply_opcompaction(const TxnLogPB_OpCompaction& op_compa
761787
file_meta.set_name(entry.index_file());
762788
if (entry.has_file_size()) file_meta.set_size(entry.file_size());
763789
if (entry.has_shared_file()) file_meta.set_shared(entry.shared_file());
790+
file_meta.set_version(entry.version());
764791
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
765792
}
766793
}
@@ -866,6 +893,9 @@ void MetaFileBuilder::apply_opcompaction_with_conflict(const TxnLogPB_OpCompacti
866893
// tablets. Encode bundling into the orphan's `shared` flag so vacuum's alive-check protects
867894
// it instead of deleting a bundle file a sibling still references.
868895
file_meta.set_shared(segment_meta.shared() || segment_meta.has_bundle_file_offset());
896+
// These segments are produced and discarded within this txn, so their creation version is
897+
// the metadata version being built.
898+
file_meta.set_version(_tablet_meta->version());
869899
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
870900
}
871901
}
@@ -982,6 +1012,7 @@ Status MetaFileBuilder::_finalize_delvec(int64_t version, int64_t txn_id) {
9821012
if (refered_versions.find(itr->first) == refered_versions.end()) {
9831013
VLOG(2) << "Remove delvec file record from delvec meta, version: " << itr->first
9841014
<< ", file: " << itr->second.name();
1015+
itr->second.set_version(itr->first);
9851016
_tablet_meta->mutable_orphan_files()->Add(std::move(itr->second));
9861017
itr = _tablet_meta->mutable_delvec_meta()->mutable_version_to_file()->erase(itr);
9871018
} else {
@@ -1005,6 +1036,7 @@ void MetaFileBuilder::_sstable_meta_clean_after_alter_type() {
10051036
file_meta.set_name(sstable.filename());
10061037
file_meta.set_size(sstable.filesize());
10071038
file_meta.set_shared(sstable.shared());
1039+
file_meta.set_version(sstable.version());
10081040
_tablet_meta->mutable_orphan_files()->Add(std::move(file_meta));
10091041
}
10101042
// Clear the SSTable metadata.
@@ -1370,6 +1402,8 @@ Status MetaFileBuilder::set_final_rowset() {
13701402
del_file_with_rid.set_encryption_meta(del.encryption_meta());
13711403
}
13721404
del_file_with_rid.set_shared(del.shared());
1405+
// Freshly created del file: stamp its creation version (see apply_opwrite).
1406+
del_file_with_rid.set_version(rowset->version());
13731407
rowset->add_del_files()->CopyFrom(del_file_with_rid);
13741408
}
13751409

@@ -1379,7 +1413,14 @@ Status MetaFileBuilder::set_final_rowset() {
13791413
// Collect orphan files: replaced partial-update segments and their segment-name-keyed .vi files
13801414
for (const auto& orphan_file : _pending_rowset_data.orphan_files) {
13811415
DCHECK(is_segment(orphan_file.name()) || is_vector_index(orphan_file.name()));
1382-
_tablet_meta->mutable_orphan_files()->Add()->CopyFrom(orphan_file);
1416+
auto* added_orphan = _tablet_meta->mutable_orphan_files()->Add();
1417+
added_orphan->CopyFrom(orphan_file);
1418+
// These replaced segment/.vi files are produced by this txn's partial-update rewrite, so
1419+
// their creation version is the metadata version being built. Respect an accurate version if
1420+
// the producer already set one.
1421+
if (!added_orphan->has_version()) {
1422+
added_orphan->set_version(_tablet_meta->version());
1423+
}
13831424
}
13841425

13851426
// Handle schema mapping (same logic as apply_opwrite)

be/src/storage/lake/tablet_merger.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,7 @@ Status merge_idg_meta(const std::vector<TabletMergeContext>& merge_contexts, Tab
14651465
fm.set_name(e.index_file());
14661466
if (e.has_file_size()) fm.set_size(e.file_size());
14671467
if (tgt_shared) fm.set_shared(true);
1468+
fm.set_version(e.version());
14681469
}
14691470
}
14701471
if (ver.entries_size() == 0) continue;

be/src/storage/lake/tablet_retain_info.cpp

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,23 @@
1414

1515
#include "storage/lake/tablet_retain_info.h"
1616

17-
#include "common/status.h"
18-
#include "storage/lake/tablet_manager.h"
19-
#include "storage/lake/tablet_metadata.h"
20-
2117
namespace starrocks::lake {
2218

23-
Status TabletRetainInfo::init(int64_t tablet_id, const std::unordered_set<int64_t>& retain_versions,
24-
TabletManager* tablet_mgr) {
25-
_tablet_id = tablet_id;
26-
for (const auto& version : retain_versions) {
27-
auto res = tablet_mgr->get_tablet_metadata(tablet_id, version, false);
28-
if (!res.status().ok()) {
29-
return res.status();
30-
}
31-
auto metadata = std::move(res).value();
32-
for (const auto& rowset : metadata->rowsets()) {
33-
_rowset_ids.insert(rowset.id());
34-
}
19+
void TabletRetainInfo::init(const std::unordered_set<int64_t>& retain_versions) {
20+
_retain_versions = retain_versions;
21+
}
3522

36-
for (const auto& [_, dcg] : (*metadata).dcg_meta().dcgs()) {
37-
for (const auto& column_file : dcg.column_files()) {
38-
_files.insert(column_file);
39-
}
40-
}
41-
for (const auto& [_, file] : (*metadata).delvec_meta().version_to_file()) {
42-
_files.insert(file.name());
43-
}
44-
for (const auto& sstable : (*metadata).sstable_meta().sstables()) {
45-
_files.insert(sstable.filename());
23+
bool TabletRetainInfo::retained_by_version(int64_t create_version, int64_t remove_version) const {
24+
for (auto version : _retain_versions) {
25+
if (create_version <= version && version < remove_version) {
26+
return true;
4627
}
47-
48-
_versions.insert(version);
4928
}
50-
return Status::OK();
51-
}
52-
53-
bool TabletRetainInfo::contains_file(const std::string& file_name) const {
54-
return _files.find(file_name) != _files.end();
29+
return false;
5530
}
5631

5732
bool TabletRetainInfo::contains_version(int64_t version) const {
58-
return _versions.find(version) != _versions.end();
59-
}
60-
61-
bool TabletRetainInfo::contains_rowset(uint32_t rowset_id) const {
62-
return _rowset_ids.find(rowset_id) != _rowset_ids.end();
33+
return _retain_versions.find(version) != _retain_versions.end();
6334
}
6435

6536
} // namespace starrocks::lake

be/src/storage/lake/tablet_retain_info.h

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,40 @@
1515
#pragma once
1616

1717
#include <cstdint>
18-
#include <string>
1918
#include <unordered_set>
2019

21-
namespace starrocks {
22-
class Status;
23-
}
24-
2520
namespace starrocks::lake {
2621

27-
class TabletManager;
28-
2922
/*
30-
* TabletRetainInfo is used to collect all files name, rowsets id for the specifed versions
31-
* that used in vacuum process to determine which files(data or meta) can be deleted.
23+
* TabletRetainInfo carries the set of tablet metadata versions that lake vacuum must retain for a
24+
* partition (e.g. versions pinned by a cluster snapshot). It answers two questions used while
25+
* collecting garbage:
26+
* - whether a tablet metadata file at a given version must be kept (contains_version);
27+
* - whether a data file must be kept because it was live at a retained version
28+
* (retained_by_version), decided purely from the file's own [create, remove) version interval.
29+
*
30+
* It deliberately reads no per-version tablet metadata: a retained version may predate the tablet
31+
* (e.g. a split/merge child asked to retain a pre-reshard version it never had), so reading it would
32+
* fail with NotFound. Keying on the file's own version instead makes the decision independent of
33+
* both that read and the per-file `shared` flag (which reshard does not set uniformly).
3234
*/
3335
class TabletRetainInfo {
3436
public:
3537
TabletRetainInfo() = default;
3638
~TabletRetainInfo() = default;
3739

38-
Status init(int64_t tablet_id, const std::unordered_set<int64_t>& retain_versions, TabletManager* tablet_mgr);
40+
void init(const std::unordered_set<int64_t>& retain_versions);
3941

40-
bool contains_file(const std::string& file_name) const;
42+
// Whether a data file created at |create_version| and removed at |remove_version| was live at
43+
// some retained version, i.e. there exists a retained version V with
44+
// create_version <= V < remove_version. A |create_version| of 0 means "unknown" and is treated
45+
// as the earliest possible version, so the file is retained conservatively.
46+
bool retained_by_version(int64_t create_version, int64_t remove_version) const;
4147

4248
bool contains_version(int64_t version) const;
4349

44-
bool contains_rowset(uint32_t rowset_id) const;
45-
46-
int64_t tablet_id() const { return _tablet_id; }
47-
4850
private:
49-
int64_t _tablet_id;
50-
std::unordered_set<int64_t> _versions;
51-
std::unordered_set<std::string> _files;
52-
std::unordered_set<uint32_t> _rowset_ids;
51+
std::unordered_set<int64_t> _retain_versions;
5352
};
5453

5554
} // namespace starrocks::lake

be/src/storage/lake/txn_log_applier.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ void collect_dcg_orphan_files(const DeltaColumnGroupMetadataPB& old_dcg_meta,
235235
if (dcg_ver.shared_files_size() > 0 && i < dcg_ver.shared_files_size()) {
236236
file_meta.set_shared(dcg_ver.shared_files(i));
237237
}
238+
if (i < dcg_ver.versions_size()) {
239+
file_meta.set_version(dcg_ver.versions(i));
240+
}
238241
metadata->mutable_orphan_files()->Add(std::move(file_meta));
239242
}
240243
}
@@ -257,6 +260,7 @@ void collect_idg_orphan_files(const IndexDeltaGroupMetadataPB& old_idg_meta,
257260
file_meta.set_name(entry.index_file());
258261
if (entry.has_file_size()) file_meta.set_size(entry.file_size());
259262
file_meta.set_shared(entry.shared_file());
263+
file_meta.set_version(entry.version());
260264
metadata->mutable_orphan_files()->Add(std::move(file_meta));
261265
}
262266
}
@@ -311,6 +315,7 @@ class PrimaryKeyTxnLogApplier : public TxnLogApplier {
311315
file_meta.set_name(sstable.filename());
312316
file_meta.set_size(sstable.filesize());
313317
file_meta.set_shared(sstable.shared());
318+
file_meta.set_version(sstable.version());
314319
_metadata->mutable_orphan_files()->Add(std::move(file_meta));
315320
}
316321
_metadata->clear_sstable_meta();
@@ -637,7 +642,12 @@ class PrimaryKeyTxnLogApplier : public TxnLogApplier {
637642
// Cleanup orphan lcrm files from merged large rowset split subtasks
638643
// These files are no longer valid after merging (segment IDs changed)
639644
for (const auto& lcrm_file : op_parallel.orphan_lcrm_files()) {
640-
_metadata->add_orphan_files()->CopyFrom(lcrm_file);
645+
auto* added = _metadata->add_orphan_files();
646+
added->CopyFrom(lcrm_file);
647+
// Transient mapper file produced and orphaned by this compaction, never referenced by
648+
// visible metadata; stamp its creation version so vacuum can reclaim it rather than
649+
// over-retaining it under a covering snapshot.
650+
added->set_version(_new_version);
641651
}
642652

643653
return Status::OK();
@@ -827,6 +837,7 @@ class PrimaryKeyTxnLogApplier : public TxnLogApplier {
827837
file_meta.set_name(file.name());
828838
file_meta.set_size(file.size());
829839
file_meta.set_shared(file.shared());
840+
file_meta.set_version(version);
830841
_metadata->mutable_orphan_files()->Add(std::move(file_meta));
831842
}
832843
// Clear sstable_meta and add to orphan files.
@@ -836,6 +847,7 @@ class PrimaryKeyTxnLogApplier : public TxnLogApplier {
836847
file_meta.set_name(sstable.filename());
837848
file_meta.set_size(sstable.filesize());
838849
file_meta.set_shared(sstable.shared());
850+
file_meta.set_version(sstable.version());
839851
_metadata->mutable_orphan_files()->Add(std::move(file_meta));
840852
}
841853
collect_dcg_orphan_files(old_dcg_meta, new_referenced_files, _metadata.get());

0 commit comments

Comments
 (0)