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

hello-stephen 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 07e355865eb [fix](cloud) Fix schema change version hole test fixture 
(#66669)
07e355865eb is described below

commit 07e355865eb5416c6be76b4d3af076f496153c21
Author: Gavin Chou <[email protected]>
AuthorDate: Thu Aug 13 10:21:31 2026 +0800

    [fix](cloud) Fix schema change version hole test fixture (#66669)
    
    ## Proposed changes
    
    Fix `CloudSchemaChangeJobTest.FillVersionHolesBeforeNewTabletRunning` by
    making the test fixture match the current cloud schema change flow:
    
    - inject the base tablet `[2-2]` rowset returned by the mocked schema
    change `alter_version=2`
    - provide a local-backed remote filesystem for cloud rowset writer
    initialization
    - mock `prepare_rowset`/`commit_rowset` for historical rowset conversion
    - make zero-segment fixture rowsets internally consistent (`empty=true`,
    `num_rows=0`, `newest_write_timestamp` set)
    
    ## Problem
    
    The test mocked `prepare_tablet_job` with `alter_version=2`, but only
    injected rowsets into the new tablet. Current master refreshes and reads
    the base tablet historical rowsets for `[2, alter_version]`, so the test
    failed with `failed to find path in version_graph. spec_version: 2-2`.
    
    ## Test intent and correctness
    
    This regression models the schema change finalization path with the
    following version layout:
    
    - the FE request still carries `alter_version=1`
    - MetaService returns the authoritative registered boundary `V1=2`
    - the base tablet contains historical version `[2-2]`
    - the new tablet contains the placeholder `[0-1]` and a post-V1 rowset
    `[4-4]`, leaving version 3 missing
    - schema change converts `[2-2]`, then finalization must create the
    `[3-3]` hole rowset so that `[3,4]` is capturable before the tablet is
    exposed as RUNNING
    
    The added base `[2-2]` rowset is a required precondition, not the
    expected result being injected. A registered `V1=2` means the base
    tablet must provide a continuous path through version 2. The old fixture
    returned `V1=2` while leaving the base tablet empty, which is
    inconsistent with the production contract and only happened to work
    while the capture gate incorrectly used the stale FE request version.
    
    The test oracle remains independent from the fixture repair:
    
    - the mock injects `[0-1]`, `[2-2]`, and `[4-4]`, but never injects the
    target `[3-3]`
    - `[3-3]` can only be produced by the production `fill_version_holes()`
    path
    - the test verifies that `[3-3]` is both empty and marked as a hole
    rowset
    - it also verifies that `capture_consistent_versions([3,4])` returns
    `[3-3]` followed by `[4-4]`
    - removing the production hole-filling call makes these assertions fail
    
    The zero-row, zero-segment base rowset is intentional and internally
    consistent. This test focuses on version-graph finalization rather than
    payload transformation, so it uses a valid empty historical rowset
    without requiring physical segment files. The local-backed remote
    filesystem and `prepare_rowset`/`commit_rowset` callbacks only provide
    the infrastructure now required because the registered V1 correctly
    enters historical conversion; they do not create the hole under test.
    
    This PR changes only the fixture. It does not change production
    behavior. The existing test validates the observable graph repair after
    schema change finalization; it is not intended to validate row-content
    transformation or add a new concurrency/order oracle.
    
    ## Validation
    
    - `sh format_code.sh be/test/cloud/cloud_schema_change_job_test.cpp`
    - `sh run-be-ut.sh --run --filter
    "CloudSchemaChangeJobTest.FillVersionHolesBeforeNewTabletRunning"`
    - `sh run-be-ut.sh --run --filter "CloudSchemaChangeJobTest.*"`
    
    Jira: DORIS-27883
    
    Co-authored-by: gavinchou <[email protected]>
---
 be/test/cloud/cloud_schema_change_job_test.cpp | 101 ++++++++++++++++++++++++-
 1 file changed, 98 insertions(+), 3 deletions(-)

diff --git a/be/test/cloud/cloud_schema_change_job_test.cpp 
b/be/test/cloud/cloud_schema_change_job_test.cpp
index 74b27bed7ad..9afce0bfb5c 100644
--- a/be/test/cloud/cloud_schema_change_job_test.cpp
+++ b/be/test/cloud/cloud_schema_change_job_test.cpp
@@ -30,6 +30,8 @@
 #include "cloud/cloud_tablet_mgr.h"
 #include "common/status.h"
 #include "cpp/sync_point.h"
+#include "io/fs/local_file_system.h"
+#include "io/fs/remote_file_system.h"
 #include "json2pb/json_to_pb.h"
 #include "storage/rowset/rowset_factory.h"
 #include "storage/rowset/rowset_meta.h"
@@ -38,6 +40,78 @@
 
 namespace doris {
 
+namespace {
+
+class LocalRemoteFileSystem final : public io::RemoteFileSystem {
+public:
+    explicit LocalRemoteFileSystem(std::string root_path)
+            : RemoteFileSystem(std::move(root_path), 
"cloud_schema_change_job_test_fs",
+                               io::FileSystemType::BROKER) {}
+
+private:
+    Status create_file_impl(const io::Path& file, io::FileWriterPtr* writer,
+                            const io::FileWriterOptions* opts) override {
+        return io::global_local_filesystem()->create_file(file, writer, opts);
+    }
+
+    Status create_directory_impl(const io::Path& dir, bool failed_if_exists) 
override {
+        return io::global_local_filesystem()->create_directory(dir, 
failed_if_exists);
+    }
+
+    Status delete_file_impl(const io::Path& file) override {
+        return io::global_local_filesystem()->delete_file(file);
+    }
+
+    Status batch_delete_impl(const std::vector<io::Path>& files) override {
+        return io::global_local_filesystem()->batch_delete(files);
+    }
+
+    Status delete_directory_impl(const io::Path& dir) override {
+        return io::global_local_filesystem()->delete_directory(dir);
+    }
+
+    Status exists_impl(const io::Path& path, bool* res) const override {
+        return io::global_local_filesystem()->exists(path, res);
+    }
+
+    Status file_size_impl(const io::Path& file, int64_t* file_size) const 
override {
+        return io::global_local_filesystem()->file_size(file, file_size);
+    }
+
+    Status list_impl(const io::Path& dir, bool only_file, 
std::vector<io::FileInfo>* files,
+                     bool* exists) override {
+        return io::global_local_filesystem()->list(dir, only_file, files, 
exists);
+    }
+
+    Status rename_impl(const io::Path& orig_name, const io::Path& new_name) 
override {
+        return io::global_local_filesystem()->rename(orig_name, new_name);
+    }
+
+    Status upload_impl(const io::Path& local_file, const io::Path& 
remote_file) override {
+        return io::global_local_filesystem()->link_file(local_file, 
remote_file);
+    }
+
+    Status batch_upload_impl(const std::vector<io::Path>& local_files,
+                             const std::vector<io::Path>& remote_files) 
override {
+        DCHECK_EQ(local_files.size(), remote_files.size());
+        for (size_t i = 0; i < local_files.size(); ++i) {
+            RETURN_IF_ERROR(upload_impl(local_files[i], remote_files[i]));
+        }
+        return Status::OK();
+    }
+
+    Status download_impl(const io::Path& remote_file, const io::Path& 
local_file) override {
+        return io::global_local_filesystem()->link_file(remote_file, 
local_file);
+    }
+
+    Status open_file_internal(const io::Path& file, io::FileReaderSPtr* reader,
+                              const io::FileReaderOptions& opts) override {
+        return io::global_local_filesystem()->open_file(file, reader, &opts);
+    }
+};
+
+} // namespace
+
 class CloudSchemaChangeJobTest : public testing::Test {
 public:
     CloudSchemaChangeJobTest() : _engine(CloudStorageEngine(EngineOptions {})) 
{}
@@ -46,6 +120,7 @@ public:
         _cluster_info = std::make_shared<CloudClusterInfo>();
         _cluster_info->_is_in_standby = false;
         ExecEnv::GetInstance()->_cluster_info = _cluster_info.get();
+        _engine.set_latest_fs(std::make_shared<LocalRemoteFileSystem>("/tmp"));
 
         _json_rowset_meta = R"({
             "rowset_id": 540081,
@@ -56,11 +131,11 @@ public:
             "rowset_state": "VISIBLE",
             "start_version": 2,
             "end_version": 2,
-            "num_rows": 100,
+            "num_rows": 0,
             "total_disk_size": 41,
             "data_disk_size": 41,
             "index_disk_size": 235,
-            "empty": false,
+            "empty": true,
             "load_id": {
                 "hi": -5350970832824939812,
                 "lo": -6717994719194512122
@@ -87,6 +162,7 @@ protected:
         auto rs_meta = std::make_shared<RowsetMeta>();
         rs_meta->init_from_pb(pb);
         rs_meta->set_tablet_schema(schema);
+        rs_meta->set_newest_write_timestamp(1553765670);
         RowsetSharedPtr rowset;
         static_cast<void>(RowsetFactory::create_rowset(schema, "", rs_meta, 
&rowset));
         return rowset;
@@ -112,8 +188,10 @@ TEST_F(CloudSchemaChangeJobTest, 
FillVersionHolesBeforeNewTabletRunning) {
     auto new_tablet = std::make_shared<CloudTablet>(_engine, 
std::move(new_meta));
     static_cast<void>(new_tablet->set_tablet_state(TABLET_NOTREADY));
 
+    auto base_rowset = create_rowset(base_tablet->tablet_schema(), 
base_tablet_id, 2, 2);
     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(base_rowset, nullptr);
     ASSERT_NE(placeholder, nullptr);
     ASSERT_NE(rowset_after_hole, nullptr);
 
@@ -135,7 +213,12 @@ TEST_F(CloudSchemaChangeJobTest, 
FillVersionHolesBeforeNewTabletRunning) {
     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) {
+        if (tablet->tablet_id() == base_tablet_id) {
+            std::unique_lock lock(tablet->get_header_lock());
+            if (!tablet->rowset_map().count(Version(2, 2))) {
+                tablet->add_rowsets({base_rowset}, false, lock, false);
+            }
+        } else if (tablet->tablet_id() == new_tablet_id) {
             loaded_new_tablet = tablet;
             std::unique_lock lock(tablet->get_header_lock());
             std::vector<RowsetSharedPtr> rowsets;
@@ -162,6 +245,18 @@ TEST_F(CloudSchemaChangeJobTest, 
FillVersionHolesBeforeNewTabletRunning) {
         resp->set_alter_version(2);
     });
 
+    sp->set_call_back("CloudMetaMgr::prepare_rowset", [](auto&& outcome) {
+        auto* pairs = try_any_cast_ret<Status>(outcome);
+        pairs->second = true;
+        pairs->first = Status::OK();
+    });
+
+    sp->set_call_back("CloudMetaMgr::commit_rowset", [](auto&& outcome) {
+        auto* pairs = try_any_cast_ret<Status>(outcome);
+        pairs->second = true;
+        pairs->first = Status::OK();
+    });
+
     bool commit_called = false;
     sp->set_call_back("CloudMetaMgr::commit_tablet_job", [&](auto&& outcome) {
         commit_called = true;


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

Reply via email to