Skip to content

Commit 01a579c

Browse files
xiangguangyxgclaude
andcommitted
[Enhancement] Record generation version on lake PK index sstables
Reuse PersistentIndexSstablePB.version (field 1, previously deprecated and unused) to record, for each physical PK-index sstable, the tablet metadata version at which it first became visible. Segment (rowset.version), delvec, and DCG already carry a reliable version; PK-index sstables did not. This is open-source infrastructure so that a version-based incremental copy has a reliable per-file version for PK sstables. - LakePersistentIndex::commit stamps each sstable via assign_new_versions: a file already present in the previous persisted sstable_meta keeps its recorded version -- so a legacy/pre-feature file (never stamped) stays 0 and lake vacuum retention treats it conservatively -- a genuinely-new file (absent from the previous meta) gets the new version, and a non-zero value is never overwritten. This one choke point covers normal flush, all compaction variants, and ingest. - The reshard PK-memtable flush runs commit() at base_version, so it threads the reshard new_version as an explicit parameter (commit() otherwise defaults to the builder's publish version), stamping freshly-flushed sstables with new_version without mutating the metadata version. - Merge builds sstable_meta directly (bypasses commit): rebuild outputs are new physical files stamped new_version at their emit sites, while projection paths reuse the file and inherit the version via CopyFrom. - ingest_sst stamps the ingested sstable's version. - proto: document field 1 and fix the stale PersistentIndexSstableMetaPB ordering comment (ordered by max_rss_rowid). 0/unset means unknown; consumers such as lake vacuum retention must treat it conservatively. Cross-cluster replication is out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: xiangguangyxg <xiangguangyxg@gmail.com>
1 parent 16c0f50 commit 01a579c

11 files changed

Lines changed: 366 additions & 12 deletions

be/src/storage/lake/lake_persistent_index.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

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

17+
#include <unordered_map>
18+
1719
#include "base/debug/trace.h"
1820
#include "base/utility/defer_op.h"
1921
#include "column/chunk_factory.h"
@@ -246,6 +248,10 @@ Status LakePersistentIndex::ingest_sst(const FileMetaPB& sst_meta, const Persist
246248
sstable_pb.set_filesize(sst_meta.size());
247249
sstable_pb.set_shared_rssid(rssid);
248250
sstable_pb.set_shared_version(version);
251+
// Record the version (PersistentIndexSstablePB.version) at which this ingested
252+
// sstable becomes visible in this tablet. Coexists with shared_version
253+
// (read-time projection) and does not affect projection.
254+
sstable_pb.set_version(version);
249255
sstable_pb.set_encryption_meta(sst_meta.encryption_meta());
250256
// Preserve the shared flag from sst_meta. During tablet split cross-publish,
251257
// newly-ingested SSTs may be shared with sibling split tablets; losing this flag
@@ -873,7 +879,31 @@ Status LakePersistentIndex::apply_opcompaction(const TabletMetadataPtr& metadata
873879
return Status::OK();
874880
}
875881

876-
Status LakePersistentIndex::commit(MetaFileBuilder* builder) {
882+
void LakePersistentIndex::assign_new_versions(PersistentIndexSstableMetaPB* new_meta,
883+
const PersistentIndexSstableMetaPB& prev_meta, int64_t new_version) {
884+
// Callers resolve |new_version| to a real publish/reshard version first; a new file must
885+
// never be stamped 0 ("unknown"). Benign if it ever slips through in release -- 0 is the
886+
// conservative value consumers treat as retain -- so a debug assert suffices.
887+
DCHECK_GT(new_version, 0);
888+
std::unordered_map<std::string, int64_t> prev_versions;
889+
prev_versions.reserve(prev_meta.sstables_size());
890+
for (const auto& s : prev_meta.sstables()) {
891+
prev_versions.emplace(s.filename(), s.version());
892+
}
893+
for (auto& s : *new_meta->mutable_sstables()) {
894+
if (s.version() != 0) {
895+
continue;
896+
}
897+
auto it = prev_versions.find(s.filename());
898+
if (it != prev_versions.end()) {
899+
s.set_version(it->second);
900+
} else {
901+
s.set_version(new_version);
902+
}
903+
}
904+
}
905+
906+
Status LakePersistentIndex::commit(MetaFileBuilder* builder, int64_t new_version) {
877907
if ((too_many_rebuild_files() || too_many_rebuild_rows()) && !_memtable->empty()) {
878908
// If we have too many files or rows need to be rebuilt,
879909
// we need to do flush to reduce index rebuild cost later.
@@ -896,6 +926,14 @@ Status LakePersistentIndex::commit(MetaFileBuilder* builder) {
896926
new_sstable_pb->CopyFrom(sstable_pb);
897927
}
898928
}
929+
// new_version == 0 means "use this publish's version"; a non-zero override is passed by
930+
// the reshard flush (see UpdateManager::flush_pk_memtable). The builder still holds the
931+
// previous persisted sstable_meta here (before finalize_sstable_meta below), which
932+
// assign_new_versions uses to carry existing versions forward.
933+
if (new_version == 0) {
934+
new_version = builder->tablet_meta()->version();
935+
}
936+
assign_new_versions(&sstable_meta, builder->tablet_meta()->sstable_meta(), new_version);
899937
builder->finalize_sstable_meta(sstable_meta);
900938
auto [file_cnt, row_cnt] = need_rebuild_counts(*builder->tablet_meta(), sstable_meta);
901939
_need_rebuild_file_cnt = file_cnt;

be/src/storage/lake/lake_persistent_index.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class LakePersistentIndex : public PersistentIndex {
121121

122122
Status apply_opcompaction(const TabletMetadataPtr& metadata, const TxnLogPB_OpCompaction& op_compaction);
123123

124-
Status commit(MetaFileBuilder* builder);
124+
Status commit(MetaFileBuilder* builder, int64_t new_version = 0);
125125

126126
Status load_from_lake_tablet(TabletManager* tablet_mgr, const TabletMetadataPtr& metadata, int64_t base_version,
127127
const MetaFileBuilder* builder);
@@ -138,6 +138,14 @@ class LakePersistentIndex : public PersistentIndex {
138138
static void pick_sstables_for_merge(const PersistentIndexSstableMetaPB& sstable_meta,
139139
std::vector<PersistentIndexSstablePB>* sstables, bool* merge_base_level);
140140

141+
// Assign the version (PersistentIndexSstablePB.version) to each sstable in |new_meta|: a
142+
// sstable that already carries a non-zero version is left untouched; a file present in
143+
// |prev_meta| keeps its recorded version (a legacy 0 stays 0, so lake vacuum retention
144+
// treats it conservatively); a file absent from |prev_meta| is new this publish and is
145+
// stamped with |new_version|.
146+
static void assign_new_versions(PersistentIndexSstableMetaPB* new_meta,
147+
const PersistentIndexSstableMetaPB& prev_meta, int64_t new_version);
148+
141149
// Check if this rowset need to rebuild, return `True` means need to rebuild this rowset.
142150
static bool needs_rowset_rebuild(const RowsetMetadataPB& rowset, uint32_t rebuild_rss_id);
143151

be/src/storage/lake/lake_primary_index.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ Status LakePrimaryIndex::ingest_sst(const FileMetaPB& sst_meta, const Persistent
228228
}
229229
}
230230

231-
Status LakePrimaryIndex::commit(const TabletMetadataPtr& metadata, MetaFileBuilder* builder) {
231+
Status LakePrimaryIndex::commit(const TabletMetadataPtr& metadata, MetaFileBuilder* builder, int64_t new_version) {
232232
TRACE_COUNTER_SCOPE_LATENCY_US("primary_index_commit_latency_us");
233233
if (!_enable_persistent_index) {
234234
return Status::OK();
@@ -251,7 +251,7 @@ Status LakePrimaryIndex::commit(const TabletMetadataPtr& metadata, MetaFileBuild
251251
case PersistentIndexTypePB::CLOUD_NATIVE: {
252252
auto* lake_persistent_index = dynamic_cast<LakePersistentIndex*>(_persistent_index.get());
253253
if (lake_persistent_index != nullptr) {
254-
return lake_persistent_index->commit(builder);
254+
return lake_persistent_index->commit(builder, new_version);
255255
} else {
256256
return Status::InternalError("Persistent index is not a LakePersistentIndex.");
257257
}

be/src/storage/lake/lake_primary_index.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class LakePrimaryIndex : public PrimaryIndex {
7171

7272
Status apply_opcompaction(const TabletMetadataPtr& metadata, const TxnLogPB_OpCompaction& op_compaction);
7373

74-
Status commit(const TabletMetadataPtr& metadata, MetaFileBuilder* builder);
74+
Status commit(const TabletMetadataPtr& metadata, MetaFileBuilder* builder, int64_t new_version = 0);
7575

7676
// Force any in-memory memtables of the cloud-native persistent index to
7777
// be flushed into sstables on shared storage. A no-op for LOCAL /

be/src/storage/lake/tablet_merger.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2665,6 +2665,8 @@ Status emit_legacy_shared_sstable_into_dest(TabletManager* tablet_manager, const
26652665
RETURN_IF_ERROR(rebuild_legacy_shared_sstable(tablet_manager, new_metadata.id(), src_pb, ctx.metadata(),
26662666
new_metadata, *family_maps_ptr, &projected_pb));
26672667
if (!projected_pb.filename().empty()) {
2668+
// Rebuilt = a brand-new physical file, first visible at the merge version.
2669+
projected_pb.set_version(new_metadata.version());
26682670
dest->Add()->Swap(&projected_pb);
26692671
}
26702672
return Status::OK();
@@ -2690,6 +2692,8 @@ Status emit_non_shared_legacy_sstable_into_dest(TabletManager* tablet_manager, c
26902692
RETURN_IF_ERROR(rebuild_non_shared_legacy_sstable(tablet_manager, new_metadata.id(), src_pb, ctx,
26912693
ctx.metadata(), &rebuilt_pb));
26922694
if (!rebuilt_pb.filename().empty()) {
2695+
// Rebuilt = a brand-new physical file, first visible at the merge version.
2696+
rebuilt_pb.set_version(new_metadata.version());
26932697
dest->Add()->Swap(&rebuilt_pb);
26942698
}
26952699
return Status::OK();
@@ -2741,7 +2745,8 @@ Status merge_sstables(TabletManager* tablet_manager, std::vector<TabletMergeCont
27412745
// the case where an old tablet accumulated post-split DML that never
27422746
// reached shared storage before merge; see the symmetric call in
27432747
// split_tablet for the pre-split side of the invariant.
2744-
ASSIGN_OR_RETURN(auto flushed_metadata, update_manager->flush_pk_memtable(ctx.metadata()));
2748+
ASSIGN_OR_RETURN(auto flushed_metadata,
2749+
update_manager->flush_pk_memtable(ctx.metadata(), new_metadata->version()));
27452750
ctx.set_metadata(std::move(flushed_metadata));
27462751
if (!ctx.metadata()->has_sstable_meta()) continue;
27472752

be/src/storage/lake/tablet_splitter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1547,7 +1547,7 @@ StatusOr<std::unordered_map<int64_t, MutableTabletMetadataPtr>> split_tablet(
15471547
// the "reshard inputs must have full sstable coverage" invariant; merge
15481548
// does the post-split half in merge_sstables.
15491549
ASSIGN_OR_RETURN(TabletMetadataPtr old_tablet_metadata,
1550-
tablet_manager->update_mgr()->flush_pk_memtable(tablet_metadata));
1550+
tablet_manager->update_mgr()->flush_pk_memtable(tablet_metadata, new_version));
15511551

15521552
// Dispatch on FE-supplied new_tablet_ranges. When set, FE has computed the
15531553
// K-1 boundaries externally (external boundaries / external boundaries); BE computes

be/src/storage/lake/update_manager.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ void UpdateManager::unload_and_remove_primary_index(int64_t tablet_id) {
233233
}
234234
}
235235

236-
StatusOr<TabletMetadataPtr> UpdateManager::flush_pk_memtable(const TabletMetadataPtr& metadata) {
236+
StatusOr<TabletMetadataPtr> UpdateManager::flush_pk_memtable(const TabletMetadataPtr& metadata, int64_t new_version) {
237237
if (!is_primary_key(*metadata) || !metadata->enable_persistent_index() ||
238238
metadata->persistent_index_type() != PersistentIndexTypePB::CLOUD_NATIVE) {
239239
return metadata;
@@ -277,7 +277,12 @@ StatusOr<TabletMetadataPtr> UpdateManager::flush_pk_memtable(const TabletMetadat
277277
// mutable_metadata->sstable_meta(). builder.finalize() is NOT called
278278
// here — this flush does not produce its own metadata version; the
279279
// returned metadata is consumed by the surrounding reshard publish.
280-
RETURN_IF_ERROR(index_entry->value().commit(metadata, &builder));
280+
// This flush runs at base_version, but its freshly-flushed sstables first become
281+
// visible to the split/merge children at the reshard publish version, so pass
282+
// new_version to commit(): it stamps those (new) sstables with new_version while
283+
// inherited sstables (already present in the pre-commit sstable_meta) keep their
284+
// recorded versions. The metadata version is left untouched.
285+
RETURN_IF_ERROR(index_entry->value().commit(metadata, &builder, new_version));
281286

282287
// Success: dismiss the failure cleanup and release the cache entry.
283288
// write_guard is released when it goes out of scope at function return.

be/src/storage/lake/update_manager.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ class UpdateManager {
221221
// exclusion between publish_resharding_tablet and publish_version, so
222222
// the cached _data_version cannot advance past metadata->version()
223223
// during this call.
224-
StatusOr<TabletMetadataPtr> flush_pk_memtable(const TabletMetadataPtr& metadata);
224+
StatusOr<TabletMetadataPtr> flush_pk_memtable(const TabletMetadataPtr& metadata, int64_t new_version);
225225

226226
StatusOr<IndexEntry*> rebuild_primary_index(const TabletMetadataPtr& metadata, MetaFileBuilder* builder,
227227
int64_t base_version, int64_t new_version,

be/test/storage/lake/lake_persistent_index_test.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@ TEST_F(LakePersistentIndexTest, test_ingest_sst_preserves_shared_flag_for_new_ss
13381338
if (sst.filename() == sst_filename) {
13391339
match_count++;
13401340
EXPECT_TRUE(sst.shared()) << "sst_meta.shared=true must be preserved in committed sstable_meta";
1341+
EXPECT_EQ(1, sst.version()) << "ingest_sst stamps the version (the ingest version)";
13411342
}
13421343
}
13431344
EXPECT_EQ(match_count, 1);
@@ -1581,4 +1582,101 @@ TEST_F(LakePersistentIndexTest, test_load_dels_parallel_matches_single_pass) {
15811582
}
15821583
}
15831584

1585+
TEST(AssignNewVersionsTest, stamps_new_preserves_existing_and_legacy) {
1586+
PersistentIndexSstableMetaPB prev;
1587+
{
1588+
auto* s = prev.add_sstables();
1589+
s->set_filename("existing_v3.sst");
1590+
s->set_version(3);
1591+
}
1592+
{
1593+
auto* s = prev.add_sstables();
1594+
s->set_filename("legacy_zero.sst"); // version unset == 0
1595+
}
1596+
1597+
PersistentIndexSstableMetaPB cur;
1598+
{
1599+
auto* s = cur.add_sstables();
1600+
s->set_filename("existing_v3.sst"); // reappears, version dropped by copy
1601+
}
1602+
{
1603+
auto* s = cur.add_sstables();
1604+
s->set_filename("legacy_zero.sst"); // legacy, still unset
1605+
}
1606+
{
1607+
auto* s = cur.add_sstables();
1608+
s->set_filename("fresh_new.sst"); // brand new this publish
1609+
}
1610+
{
1611+
auto* s = cur.add_sstables();
1612+
s->set_filename("ingested.sst");
1613+
s->set_version(9); // producer already stamped, absent from prev
1614+
}
1615+
{
1616+
auto* s = cur.add_sstables();
1617+
s->set_filename("existing_v3.sst");
1618+
s->set_version(7); // same filename as prev@3 but already non-zero
1619+
}
1620+
1621+
LakePersistentIndex::assign_new_versions(&cur, prev, /*new_version=*/100);
1622+
1623+
ASSERT_EQ(5, cur.sstables_size());
1624+
EXPECT_EQ(3, cur.sstables(0).version()); // existing, local 0 -> carried from prev (3)
1625+
EXPECT_EQ(0, cur.sstables(1).version()); // legacy 0 -> STAYS 0 (present in prev); vacuum retains conservatively
1626+
EXPECT_EQ(100, cur.sstables(2).version()); // new (absent from prev) -> new version
1627+
EXPECT_EQ(9, cur.sstables(3).version()); // producer-stamped non-zero -> preserved (absent from prev)
1628+
EXPECT_EQ(7, cur.sstables(4).version()); // non-zero -> NEVER overwritten, even though prev has same filename@3
1629+
}
1630+
1631+
TEST_F(LakePersistentIndexTest, test_commit_stamps_version) {
1632+
const int N = 100;
1633+
const int64_t publish_version = 7;
1634+
auto tablet_id = _tablet_metadata->id();
1635+
auto index = std::make_unique<LakePersistentIndex>(_tablet_mgr.get(), tablet_id);
1636+
ASSERT_OK(index->init(_tablet_metadata));
1637+
1638+
using Key = uint64_t;
1639+
std::vector<Key> keys(N);
1640+
std::vector<Slice> key_slices;
1641+
std::vector<IndexValue> values;
1642+
key_slices.reserve(N);
1643+
values.reserve(N);
1644+
for (int j = 0; j < N; j++) {
1645+
keys[j] = j;
1646+
key_slices.emplace_back((uint8_t*)(&keys[j]), sizeof(Key));
1647+
values.emplace_back(j * 2);
1648+
}
1649+
index->prepare(EditVersion(publish_version, 0), 0);
1650+
std::vector<IndexValue> old_values(N);
1651+
ASSERT_OK(index->upsert(N, key_slices.data(), values.data(), old_values.data()));
1652+
ASSERT_OK(index->flush_memtable(true));
1653+
ASSERT_OK(index->sync_flush_all_memtables(10000000));
1654+
1655+
Tablet tablet(_tablet_mgr.get(), tablet_id);
1656+
auto meta = std::make_shared<TabletMetadata>();
1657+
meta->CopyFrom(*_tablet_metadata);
1658+
meta->set_version(publish_version);
1659+
MetaFileBuilder builder(tablet, meta);
1660+
ASSERT_OK(index->commit(&builder));
1661+
1662+
// A freshly-flushed sstable is stamped with the publish version.
1663+
ASSERT_GE(meta->sstable_meta().sstables_size(), 1);
1664+
for (const auto& s : meta->sstable_meta().sstables()) {
1665+
EXPECT_EQ(publish_version, s.version()) << "freshly-flushed sstable must carry its publish version";
1666+
}
1667+
1668+
// Re-commit at a later version WITHOUT new data: existing sstables must keep
1669+
// their original version (no re-stamp), because they are present in
1670+
// the previous sstable_meta the builder carries forward.
1671+
auto meta2 = std::make_shared<TabletMetadata>();
1672+
meta2->CopyFrom(*meta); // carries the publish_version sstable_meta forward
1673+
meta2->set_version(publish_version + 10);
1674+
MetaFileBuilder builder2(tablet, meta2);
1675+
ASSERT_OK(index->commit(&builder2));
1676+
ASSERT_GE(meta2->sstable_meta().sstables_size(), 1);
1677+
for (const auto& s : meta2->sstable_meta().sstables()) {
1678+
EXPECT_EQ(publish_version, s.version()) << "re-commit must not re-stamp an existing sstable";
1679+
}
1680+
}
1681+
15841682
} // namespace starrocks::lake

0 commit comments

Comments
 (0)