This is an automated email from the ASF dual-hosted git repository.

gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new e4238ac87cc [fix](cloud) Fill schema change version holes before 
running (#63443)
e4238ac87cc is described below

commit e4238ac87ccf6df6a516517ea976d1c9d1b37dc6
Author: Xin Liao <[email protected]>
AuthorDate: Thu May 21 14:47:35 2026 +0800

    [fix](cloud) Fill schema change version holes before running (#63443)
    
    Problem Summary: During cloud schema change, empty rowsets created while
    calculating delete bitmap were only added to a temporary tablet. The
    real new tablet could become RUNNING with a local version graph hole
    after the alter version. Subsequent delete bitmap sync on the RUNNING
    MOW tablet captures old rowset ids before filling local holes, so it can
    fail to find a continuous version path.
    
    This change fills version holes on the real new tablet after adding
    schema change output rowsets and before switching it to RUNNING. It
    preserves the existing schema change rule that skips holes at or below
    alter_version. A unit test covers the local graph repair and verifies
    capture_consistent_versions can traverse through the filled empty
    rowset.
---
 be/src/cloud/cloud_schema_change_job.cpp       |   4 +
 be/test/cloud/cloud_schema_change_job_test.cpp | 109 +++++++++++++++++++++++++
 be/test/cloud/cloud_tablet_test.cpp            |  50 ++++++++++++
 3 files changed, 163 insertions(+)

diff --git a/be/src/cloud/cloud_schema_change_job.cpp 
b/be/src/cloud/cloud_schema_change_job.cpp
index f95ed81263b..018102654e0 100644
--- a/be/src/cloud/cloud_schema_change_job.cpp
+++ b/be/src/cloud/cloud_schema_change_job.cpp
@@ -573,6 +573,10 @@ Status 
CloudSchemaChangeJob::_convert_historical_rowsets(const SchemaChangeParam
             }
         }
         _new_tablet->add_rowsets(std::move(_output_rowsets), true, wlock, 
false);
+        // Ensure the real new tablet has a continuous local version graph 
before it becomes
+        // visible. Later RUNNING-tablet delete bitmap sync depends on 
capturing all old versions.
+        RETURN_IF_ERROR(_cloud_storage_engine.meta_mgr().fill_version_holes(
+                _new_tablet.get(), _new_tablet->max_version_unlocked(), 
wlock));
         _new_tablet->set_cumulative_layer_point(_output_cumulative_point);
         _new_tablet->reset_approximate_stats(stats.num_rowsets(), 
stats.num_segments(),
                                              stats.num_rows(), 
stats.data_size());
diff --git a/be/test/cloud/cloud_schema_change_job_test.cpp 
b/be/test/cloud/cloud_schema_change_job_test.cpp
index 82cfe92edac..972ff2af255 100644
--- a/be/test/cloud/cloud_schema_change_job_test.cpp
+++ b/be/test/cloud/cloud_schema_change_job_test.cpp
@@ -22,6 +22,7 @@
 #include <gtest/gtest.h>
 
 #include <memory>
+#include <vector>
 
 #include "cloud/cloud_cluster_info.h"
 #include "cloud/cloud_storage_engine.h"
@@ -96,6 +97,114 @@ protected:
     std::shared_ptr<CloudClusterInfo> _cluster_info;
 };
 
+TEST_F(CloudSchemaChangeJobTest, FillVersionHolesBeforeNewTabletRunning) {
+    int64_t base_tablet_id = 40001;
+    int64_t new_tablet_id = 40002;
+
+    TabletMetaSharedPtr base_meta(new TabletMeta(
+            1, 2, base_tablet_id, base_tablet_id + 100, 4, 5, TTabletSchema(), 
6, {{7, 8}},
+            UniqueId(9, 10), TTabletType::TABLET_TYPE_DISK, 
TCompressionType::LZ4F));
+    TabletMetaSharedPtr new_meta(new TabletMeta(
+            1, 2, new_tablet_id, new_tablet_id + 100, 4, 5, TTabletSchema(), 
6, {{7, 8}},
+            UniqueId(11, 12), TTabletType::TABLET_TYPE_DISK, 
TCompressionType::LZ4F));
+
+    auto base_tablet = std::make_shared<CloudTablet>(_engine, 
std::move(base_meta));
+    auto new_tablet = std::make_shared<CloudTablet>(_engine, 
std::move(new_meta));
+    static_cast<void>(new_tablet->set_tablet_state(TABLET_NOTREADY));
+
+    auto placeholder = create_rowset(new_tablet->tablet_schema(), 
new_tablet_id, 0, 1);
+    auto rowset_after_hole = create_rowset(new_tablet->tablet_schema(), 
new_tablet_id, 4, 4);
+    ASSERT_NE(placeholder, nullptr);
+    ASSERT_NE(rowset_after_hole, nullptr);
+
+    auto* sp = SyncPoint::get_instance();
+    sp->clear_all_call_backs();
+    sp->enable_processing();
+
+    sp->set_call_back("CloudMetaMgr::get_tablet_meta", [&](auto&& args) {
+        auto tablet_id = try_any_cast<int64_t>(args[0]);
+        auto* meta_ptr = try_any_cast<TabletMetaSharedPtr*>(args[1]);
+        if (tablet_id == base_tablet_id) {
+            *meta_ptr = base_tablet->tablet_meta();
+        } else if (tablet_id == new_tablet_id) {
+            *meta_ptr = new_tablet->tablet_meta();
+        }
+        try_any_cast_ret<Status>(args)->second = true;
+    });
+
+    CloudTablet* loaded_new_tablet = nullptr;
+    sp->set_call_back("CloudMetaMgr::sync_tablet_rowsets", [&](auto&& outcome) 
{
+        auto* tablet = try_any_cast<CloudTablet*>(outcome[0]);
+        if (tablet->tablet_id() == new_tablet_id) {
+            loaded_new_tablet = tablet;
+            std::unique_lock lock(tablet->get_header_lock());
+            std::vector<RowsetSharedPtr> rowsets;
+            if (!tablet->rowset_map().count(Version(0, 1))) {
+                rowsets.push_back(placeholder);
+            }
+            if (!tablet->rowset_map().count(Version(4, 4))) {
+                rowsets.push_back(rowset_after_hole);
+            }
+            tablet->add_rowsets(std::move(rowsets), false, lock, false);
+        }
+        auto* pairs = try_any_cast_ret<Status>(outcome);
+        pairs->second = true;
+        pairs->first = Status::OK();
+    });
+
+    sp->set_call_back("CloudMetaMgr::prepare_tablet_job", [](auto&& outcome) {
+        auto* pairs = try_any_cast_ret<Status>(outcome);
+        pairs->second = true;
+        pairs->first = Status::OK();
+
+        auto* resp = try_any_cast<cloud::StartTabletJobResponse*>(outcome[1]);
+        resp->mutable_status()->set_code(cloud::MetaServiceCode::OK);
+        resp->set_alter_version(2);
+    });
+
+    bool commit_called = false;
+    sp->set_call_back("CloudMetaMgr::commit_tablet_job", [&](auto&& outcome) {
+        commit_called = true;
+        auto* pairs = try_any_cast_ret<Status>(outcome);
+        pairs->second = true;
+        pairs->first = Status::OK();
+
+        auto* resp = try_any_cast<cloud::FinishTabletJobResponse*>(outcome[1]);
+        resp->mutable_status()->set_code(cloud::MetaServiceCode::OK);
+        auto* stats = resp->mutable_stats();
+        stats->set_num_rowsets(3);
+        stats->set_num_segments(0);
+        stats->set_num_rows(0);
+        stats->set_data_size(0);
+    });
+
+    TAlterTabletReqV2 request;
+    request.base_tablet_id = base_tablet_id;
+    request.new_tablet_id = new_tablet_id;
+    request.alter_version = 1;
+    request.__set_alter_tablet_type(TAlterTabletType::SCHEMA_CHANGE);
+
+    CloudSchemaChangeJob sc_job(_engine, "test_fill_holes_before_running", 
9999999999);
+    auto status = sc_job.process_alter_tablet(request);
+
+    ASSERT_TRUE(status.ok()) << status.to_string();
+    ASSERT_TRUE(commit_called);
+    ASSERT_NE(loaded_new_tablet, nullptr);
+    ASSERT_EQ(loaded_new_tablet->tablet_state(), TABLET_RUNNING);
+    ASSERT_TRUE(loaded_new_tablet->rowset_map().count(Version(3, 3)));
+    auto hole_rowset = loaded_new_tablet->rowset_map().at(Version(3, 3));
+    ASSERT_TRUE(hole_rowset->empty());
+    ASSERT_TRUE(hole_rowset->is_hole_rowset());
+
+    auto versions_result =
+            loaded_new_tablet->capture_consistent_versions_unlocked(Version(3, 
4), {});
+    ASSERT_TRUE(versions_result.has_value()) << versions_result.error();
+    const auto& versions = versions_result.value();
+    ASSERT_EQ(versions.size(), 2);
+    ASSERT_EQ(versions[0], Version(3, 3));
+    ASSERT_EQ(versions[1], Version(4, 4));
+}
+
 // Test: cross-V1 compaction detected → abort SC job → return 
SC_COMPACTION_CONFLICT
 TEST_F(CloudSchemaChangeJobTest, CrossV1CompactionDetected) {
     int64_t base_tablet_id = 10001;
diff --git a/be/test/cloud/cloud_tablet_test.cpp 
b/be/test/cloud/cloud_tablet_test.cpp
index d2f201b14d4..3c66f227ab3 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -26,6 +26,7 @@
 #include <cstdint>
 #include <ranges>
 
+#include "cloud/cloud_meta_mgr.h"
 #include "cloud/cloud_storage_engine.h"
 #include "cloud/cloud_warm_up_manager.h"
 #include "common/config.h"
@@ -1379,6 +1380,7 @@ public:
         rs_meta->set_rowset_type(BETA_ROWSET);
         rs_meta->set_version(version);
         rs_meta->set_rowset_id(_engine.next_rowset_id());
+        rs_meta->set_tablet_schema(_tablet->tablet_schema());
         RowsetSharedPtr rowset;
         Status st = RowsetFactory::create_rowset(nullptr, "", rs_meta, 
&rowset);
         if (!st.ok()) {
@@ -1546,6 +1548,54 @@ TEST_F(CloudTabletDeleteRowsetsForSchemaChangeTest, 
TestMultipleCompactionRowset
     ASSERT_EQ(versions[10], Version(11, 11));
 }
 
+TEST_F(CloudTabletDeleteRowsetsForSchemaChangeTest, 
TestFillVersionHolesBeforeSchemaChangeRunning) {
+    static_cast<void>(_tablet->set_tablet_state(TABLET_NOTREADY));
+    _tablet->set_alter_version(10);
+
+    auto rs_placeholder = create_rowset(Version(0, 1));
+    auto rs_historical = create_rowset(Version(2, 10));
+    auto rs_after_first_hole = create_rowset(Version(12, 12));
+    auto rs_after_second_hole = create_rowset(Version(14, 14));
+    ASSERT_NE(rs_placeholder, nullptr);
+    ASSERT_NE(rs_historical, nullptr);
+    ASSERT_NE(rs_after_first_hole, nullptr);
+    ASSERT_NE(rs_after_second_hole, nullptr);
+
+    cloud::CloudMetaMgr meta_mgr;
+    {
+        std::unique_lock wlock(_tablet->get_header_lock());
+        _tablet->add_rowsets(
+                {rs_placeholder, rs_historical, rs_after_first_hole, 
rs_after_second_hole}, false,
+                wlock, false);
+        ASSERT_FALSE(_tablet->rowset_map().count(Version(11, 11)));
+        ASSERT_FALSE(_tablet->rowset_map().count(Version(13, 13)));
+        ASSERT_FALSE(_tablet->capture_consistent_versions_unlocked(Version(0, 
14), {}).has_value());
+
+        auto status =
+                meta_mgr.fill_version_holes(_tablet.get(), 
_tablet->max_version_unlocked(), wlock);
+        ASSERT_TRUE(status.ok()) << status.to_string();
+        ASSERT_TRUE(_tablet->set_tablet_state(TABLET_RUNNING).ok());
+    }
+
+    for (const Version& version : {Version(11, 11), Version(13, 13)}) {
+        ASSERT_TRUE(_tablet->rowset_map().count(version));
+        auto hole_rowset = _tablet->rowset_map().at(version);
+        ASSERT_TRUE(hole_rowset->empty());
+        ASSERT_TRUE(hole_rowset->is_hole_rowset());
+    }
+
+    auto versions_result = 
_tablet->capture_consistent_versions_unlocked(Version(0, 14), {});
+    ASSERT_TRUE(versions_result.has_value()) << versions_result.error();
+    const auto& versions = versions_result.value();
+    ASSERT_EQ(versions.size(), 6);
+    ASSERT_EQ(versions[0], Version(0, 1));
+    ASSERT_EQ(versions[1], Version(2, 10));
+    ASSERT_EQ(versions[2], Version(11, 11));
+    ASSERT_EQ(versions[3], Version(12, 12));
+    ASSERT_EQ(versions[4], Version(13, 13));
+    ASSERT_EQ(versions[5], Version(14, 14));
+}
+
 // Reproduce the CI crash scenario: SC delete puts rowsets to stale, then
 // compaction creates a new stale path with overlapping version keys. When
 // one stale path is cleaned, the other hits DCHECK(false) because the


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to