This is an automated email from the ASF dual-hosted git repository.
lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new a774f8e6 feat(global-index): add checkpoint cleanup by task id (#465)
a774f8e6 is described below
commit a774f8e6be40579ccfe10ac6c72b72896c704c61
Author: lszskye <[email protected]>
AuthorDate: Tue Oct 27 08:51:53 2026 +0800
feat(global-index): add checkpoint cleanup by task id (#465)
---
docs/source/user_guide/global_index.rst | 16 +++
.../paimon/global_index/global_index_write_task.h | 20 ++-
.../global_index_file_manager_test.cpp | 149 +++++++++++++++++++--
.../core/global_index/global_index_write_task.cpp | 60 ++++++---
.../core/index/index_checkpoint_path_factory.h | 11 ++
src/paimon/core/utils/file_store_path_factory.cpp | 8 +-
.../core/utils/file_store_path_factory_test.cpp | 24 +++-
.../indexer/lumina/lumina_global_index_test.cpp | 9 +-
test/inte/global_index_test.cpp | 23 +++-
9 files changed, 269 insertions(+), 51 deletions(-)
diff --git a/docs/source/user_guide/global_index.rst
b/docs/source/user_guide/global_index.rst
index f3dca744..897cfebb 100644
--- a/docs/source/user_guide/global_index.rst
+++ b/docs/source/user_guide/global_index.rst
@@ -38,6 +38,22 @@ Global indexes work on top of Data Evolution tables. To use
global indexes, your
- ``'row-tracking.enabled' = 'true'``
- ``'data-evolution.enabled' = 'true'``
+Cleaning Up Build Checkpoints
+-----------------------------
+
+Checkpoint-enabled index builds remove their checkpoints after a successful
build. If a process
+crashes, use ``GlobalIndexWriteTask::DeleteCheckpoint`` to remove all
checkpoints left by that
+task once it is no longer running or being retried:
+
+.. code-block:: cpp
+
+ PAIMON_RETURN_NOT_OK(paimon::GlobalIndexWriteTask::DeleteCheckpoint(
+ table_path, "build-task-1", options, file_system));
+
+Pass the same table path and task id used in ``WriteIndex``. Task ids must be
unique within the
+table. The cleanup recursively removes the whole task directory, other tasks'
directories and completed
+index files are preserved.
+
Bitmap Index
------------
diff --git a/include/paimon/global_index/global_index_write_task.h
b/include/paimon/global_index/global_index_write_task.h
index 391c02ca..8c89a9c1 100644
--- a/include/paimon/global_index/global_index_write_task.h
+++ b/include/paimon/global_index/global_index_write_task.h
@@ -27,6 +27,7 @@
#include "paimon/global_index/indexed_split.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/result.h"
+#include "paimon/status.h"
#include "paimon/utils/range.h"
#include "paimon/visibility.h"
@@ -48,9 +49,9 @@ class PAIMON_EXPORT GlobalIndexWriteTask {
/// @param options Index-specific configuration (e.g., false positive
rate for bloom
/// filters).
/// @param task_id When checkpoints are enabled, the caller must
provide a non-empty task
- /// identifier that uniquely identifies an index build
task. Reuse it when
- /// retrying the same build. If the source data, build
configuration, or
- /// build source code changes, the caller must use a
new identifier;
+ /// identifier that uniquely identifies an index build
task within the
+ /// table. Reuse it when retrying the same build. If
the source data, build
+ /// configuration, or build source code changes, use a
new identifier;
/// otherwise, the index build may fail. Pass nullopt
when checkpoints are
/// disabled. Index types without checkpoint support
ignore this value.
/// @param pool Memory pool for temporary allocations during index
construction.
@@ -65,6 +66,19 @@ class PAIMON_EXPORT GlobalIndexWriteTask {
const std::map<std::string, std::string>& options,
const std::optional<std::string>& task_id, const
std::shared_ptr<MemoryPool>& pool,
const std::shared_ptr<FileSystem>& file_system = nullptr);
+
+ /// Deletes all checkpoints left by a global index build task. The caller
must ensure that this
+ /// task is no longer building or being retried.
+ ///
+ /// @param table_path Path to the table root directory used for the
build.
+ /// @param task_id Non-empty, unique task identifier passed to
`WriteIndex`.
+ /// @param options Configuration overrides. Used to load the schema,
then merged with
+ /// schema options, with these overrides taking
precedence.
+ /// @param file_system Specifies the file system. If `nullptr`, use
default file system.
+ /// @return OK if the task directory was deleted or does not exist.
+ static Status DeleteCheckpoint(const std::string& table_path, const
std::string& task_id,
+ const std::map<std::string, std::string>&
options,
+ const std::shared_ptr<FileSystem>&
file_system = nullptr);
};
} // namespace paimon
diff --git a/src/paimon/core/global_index/global_index_file_manager_test.cpp
b/src/paimon/core/global_index/global_index_file_manager_test.cpp
index ab27ad2e..d5cce4ad 100644
--- a/src/paimon/core/global_index/global_index_file_manager_test.cpp
+++ b/src/paimon/core/global_index/global_index_file_manager_test.cpp
@@ -20,6 +20,7 @@
#include "paimon/core/global_index/global_index_file_manager.h"
#include <limits>
+#include <map>
#include <set>
#include <vector>
@@ -27,8 +28,11 @@
#include "fmt/format.h"
#include "gtest/gtest.h"
#include "paimon/common/utils/string_utils.h"
+#include "paimon/core/schema/table_schema.h"
#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/defs.h"
#include "paimon/fs/local/local_file_system.h"
+#include "paimon/global_index/global_index_write_task.h"
#include "paimon/testing/utils/testharness.h"
#include "paimon/utils/range.h"
@@ -46,8 +50,24 @@ class GlobalIndexFileManagerTest : public ::testing::Test {
}
return LocalFileSystem::ListDir(directory, status_list);
}
+ Status Delete(const std::string& path, bool recursive = true) const
override {
+ delete_paths_.push_back(path);
+ if (path == fail_delete_path_) {
+ return Status::IOError("checkpoint delete failed");
+ }
+ return LocalFileSystem::Delete(path, recursive);
+ }
+ Result<bool> Exists(const std::string& path) const override {
+ if (path == fail_exists_path_) {
+ return Status::IOError("checkpoint exists failed");
+ }
+ return LocalFileSystem::Exists(path);
+ }
bool fail_list_ = false;
+ std::string fail_delete_path_;
+ std::string fail_exists_path_;
mutable int32_t list_count_ = 0;
+ mutable std::vector<std::string> delete_paths_;
};
void SetUp() override {
@@ -80,6 +100,17 @@ class GlobalIndexFileManagerTest : public ::testing::Test {
return PathUtil::GetName(uri);
}
+ Status CreateSchema(const std::map<std::string, std::string>& options =
{}) const {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<TableSchema> schema,
+ TableSchema::Create(/*schema_id=*/0,
+ arrow::schema({arrow::field("vector",
arrow::int32())}),
+ /*partition_keys=*/{}, /*primary_keys=*/{},
options));
+ PAIMON_ASSIGN_OR_RAISE(std::string content, schema->ToJsonString());
+ return fs_->WriteFile(PathUtil::JoinPath(dir_->Str(),
"schema/schema-0"), content,
+ /*overwrite=*/false);
+ }
+
std::shared_ptr<TrackingFileSystem> fs_ =
std::make_shared<TrackingFileSystem>();
std::unique_ptr<UniqueTestDirectory> dir_;
std::shared_ptr<FileStorePathFactory> path_factory_;
@@ -140,8 +171,8 @@ TEST_F(GlobalIndexFileManagerTest,
TestCheckpointNotConfigured) {
TEST_F(GlobalIndexFileManagerTest, TestCheckpointIOAndRestart) {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
ASSERT_EQ(fs_->list_count_, 0);
- std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint");
- std::string prefix = "lumina-global-index-vector-10-20-task-1-";
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ std::string prefix = "lumina-global-index-vector-10-20-";
std::string previous_path = PathUtil::JoinPath(directory, prefix +
"9.index.ckpt");
// The latest sequence must be discovered on first file name allocation,
not at construction.
ASSERT_OK(fs_->WriteFile(previous_path, "old", /*overwrite=*/false));
@@ -167,8 +198,8 @@ TEST_F(GlobalIndexFileManagerTest,
TestCheckpointIOAndRestart) {
ASSERT_OK_AND_ASSIGN(name, CreateCheckpointFile(restarted));
ASSERT_TRUE(StringUtils::EndsWith(name, "-12.index.ckpt"));
- std::string other_task_path =
- PathUtil::JoinPath(directory,
"lumina-global-index-vector-10-20-task-2-99.index.ckpt");
+ std::string other_task_path = PathUtil::JoinPath(
+ dir_->Str(),
"index/checkpoint/task-2/lumina-global-index-vector-10-20-99.index.ckpt");
std::string index_path = manager->ToPath("retained.index");
ASSERT_OK(fs_->WriteFile(other_task_path, "other", /*overwrite=*/false));
ASSERT_OK(fs_->WriteFile(index_path, "index", /*overwrite=*/false));
@@ -205,8 +236,8 @@ TEST_F(GlobalIndexFileManagerTest,
TestInitializationFailureCanRetry) {
TEST_F(GlobalIndexFileManagerTest, TestLatestCheckpointUsesNumericId) {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
- std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint");
- std::string prefix = "lumina-global-index-vector-10-20-task-1-";
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ std::string prefix = "lumina-global-index-vector-10-20-";
ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(directory, prefix +
"9.index.ckpt"), "older",
/*overwrite=*/false));
ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(directory, prefix +
"10.index.ckpt"), "newer",
@@ -223,8 +254,8 @@ TEST_F(GlobalIndexFileManagerTest,
TestReadsDoNotInitializeFileId) {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
ASSERT_OK_AND_ASSIGN(bool exists, manager->CheckpointExists());
ASSERT_FALSE(exists);
- std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint");
- std::string prefix = "lumina-global-index-vector-10-20-task-1-";
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ std::string prefix = "lumina-global-index-vector-10-20-";
ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(directory, prefix +
"9.index.ckpt"), "old",
/*overwrite=*/false));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> input,
manager->OpenCheckpointInputStream());
@@ -243,8 +274,8 @@ TEST_F(GlobalIndexFileManagerTest,
TestReadsDoNotInitializeFileId) {
TEST_F(GlobalIndexFileManagerTest, TestFileIdOverflow) {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
- std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint");
- std::string prefix = "lumina-global-index-vector-10-20-task-1-";
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ std::string prefix = "lumina-global-index-vector-10-20-";
int64_t max_id = std::numeric_limits<int64_t>::max();
ASSERT_OK(fs_->WriteFile(
PathUtil::JoinPath(directory, fmt::format("{}{}.index.ckpt", prefix,
max_id - 1)), "old",
@@ -291,4 +322,102 @@ TEST_F(GlobalIndexFileManagerTest,
TestDeleteDoesNotResetFileId) {
ASSERT_EQ(fs_->list_count_, 2);
}
+TEST_F(GlobalIndexFileManagerTest, TestDeleteTaskCheckpoint) {
+ ASSERT_OK(CreateSchema());
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ std::vector<std::string> matching_names =
{"lumina-global-index-vector-10-20-9.index.ckpt",
+
"lumina-global-index-vector-10-20-10.index.ckpt",
+
"lumina-global-index-other-10-20-0.index.ckpt",
+
"other-global-index-vector-10-20-0.index.ckpt",
+
"lumina-global-index-vector-10-21-0.index.ckpt",
+
"lumina-global-index-vector-11-20-0.index.ckpt",
+
"lumina-global-index-vector-10-20-0.index.ckpt.bak",
+ "incomplete-file",
+ "nested/unfinished-file"};
+ for (const std::string& name : matching_names) {
+ ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(directory, name),
"incomplete checkpoint",
+ /*overwrite=*/false));
+ }
+ std::vector<std::string> retained_tasks = {"task-2", "task-1-extended",
"task-1-9", "task-1-"};
+ std::vector<std::string> retained_paths;
+ for (const std::string& task_id : retained_tasks) {
+ std::string path = PathUtil::JoinPath(
+ dir_->Str(), "index/checkpoint/" + task_id + "/" +
matching_names.front());
+ ASSERT_OK(fs_->WriteFile(path, "retained", /*overwrite=*/false));
+ retained_paths.push_back(path);
+ }
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_));
+ ASSERT_OK_AND_ASSIGN(bool exists, fs_->Exists(directory));
+ ASSERT_FALSE(exists);
+ ASSERT_EQ(fs_->delete_paths_, std::vector<std::string>({directory}));
+ for (const std::string& path : retained_paths) {
+ ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(path));
+ ASSERT_TRUE(exists) << path;
+ }
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_));
+}
+
+TEST_F(GlobalIndexFileManagerTest,
TestDeleteTaskCheckpointWithoutCustomFileSystem) {
+ ASSERT_OK(CreateSchema());
+ ASSERT_NOK_WITH_MSG(GlobalIndexWriteTask::DeleteCheckpoint(
+ dir_->Str(), "task-1", {{Options::FILE_SYSTEM,
"unregistered-fs"}}),
+ "Create factory failed with identifier
'unregistered-fs'");
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}));
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ ASSERT_OK_AND_ASSIGN(bool exists, fs_->Exists(directory));
+ ASSERT_FALSE(exists);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
+ ASSERT_OK(CreateCheckpointFile(manager));
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}));
+ ASSERT_OK_AND_ASSIGN(exists, manager->CheckpointExists());
+ ASSERT_FALSE(exists);
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}));
+}
+
+TEST_F(GlobalIndexFileManagerTest,
TestDeleteTaskCheckpointRejectsInvalidTaskId) {
+ for (const std::string& invalid : std::vector<std::string>{"", ".", "..",
" ", "a/b", "a\\b",
+ "a\nb",
std::string("a\0b", 3)}) {
+ ASSERT_NOK(
+ GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), invalid,
/*options=*/{}, fs_));
+ }
+ ASSERT_EQ(fs_->list_count_, 0);
+ ASSERT_TRUE(fs_->delete_paths_.empty());
+}
+
+TEST_F(GlobalIndexFileManagerTest,
TestDeleteTaskCheckpointPropagatesFileSystemErrors) {
+ ASSERT_OK(CreateSchema());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager,
CreateManager());
+ ASSERT_OK_AND_ASSIGN(std::string first, CreateCheckpointFile(manager));
+ ASSERT_OK_AND_ASSIGN(std::string second, CreateCheckpointFile(manager));
+ fs_->fail_list_ = true;
+ ASSERT_NOK_WITH_MSG(
+ GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_),
+ "checkpoint list failed");
+ ASSERT_TRUE(fs_->delete_paths_.empty());
+
+ fs_->fail_list_ = false;
+ std::string directory = PathUtil::JoinPath(dir_->Str(),
"index/checkpoint/task-1");
+ fs_->fail_exists_path_ = directory;
+ ASSERT_NOK_WITH_MSG(
+ GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_),
+ "checkpoint exists failed");
+ ASSERT_TRUE(fs_->delete_paths_.empty());
+ fs_->fail_exists_path_.clear();
+ fs_->fail_delete_path_ = directory;
+ ASSERT_NOK_WITH_MSG(
+ GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_),
+ "checkpoint delete failed");
+ ASSERT_EQ(fs_->delete_paths_, std::vector<std::string>({directory}));
+ for (const std::string& name : {first, second}) {
+ ASSERT_OK_AND_ASSIGN(bool exists,
fs_->Exists(PathUtil::JoinPath(directory, name)));
+ ASSERT_TRUE(exists);
+ }
+
+ fs_->fail_delete_path_.clear();
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1",
/*options=*/{}, fs_));
+ ASSERT_OK_AND_ASSIGN(bool exists, fs_->Exists(directory));
+ ASSERT_FALSE(exists);
+}
+
} // namespace paimon::test
diff --git a/src/paimon/core/global_index/global_index_write_task.cpp
b/src/paimon/core/global_index/global_index_write_task.cpp
index d5bd13e9..f04d96bb 100644
--- a/src/paimon/core/global_index/global_index_write_task.cpp
+++ b/src/paimon/core/global_index/global_index_write_task.cpp
@@ -20,6 +20,7 @@
#include "paimon/global_index/global_index_write_task.h"
#include <set>
+#include <utility>
#include "arrow/array/array_nested.h"
#include "arrow/c/bridge.h"
@@ -34,6 +35,7 @@
#include "paimon/core/casting/casting_utils.h"
#include "paimon/core/core_options.h"
#include "paimon/core/global_index/global_index_file_manager.h"
+#include "paimon/core/index/index_checkpoint_path_factory.h"
#include "paimon/core/io/data_increment.h"
#include "paimon/core/schema/schema_manager.h"
#include "paimon/core/schema/table_schema.h"
@@ -47,6 +49,26 @@
#include "paimon/table/source/table_read.h"
namespace paimon {
namespace {
+Result<std::pair<std::shared_ptr<TableSchema>, CoreOptions>>
LoadSchemaAndOptions(
+ const std::string& table_path, const std::map<std::string, std::string>&
options,
+ const std::shared_ptr<FileSystem>& file_system) {
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions tmp_options,
CoreOptions::FromMap(options, file_system));
+ SchemaManager schema_manager(tmp_options.GetFileSystem(), table_path);
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>>
latest_table_schema,
+ schema_manager.Latest());
+ if (latest_table_schema == std::nullopt) {
+ return Status::Invalid("not found latest schema");
+ }
+ const auto& table_schema = latest_table_schema.value();
+ auto final_options = table_schema->Options();
+ for (const auto& [key, value] : options) {
+ final_options[key] = value;
+ }
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+ CoreOptions::FromMap(final_options, file_system));
+ return std::make_pair(table_schema, std::move(core_options));
+}
+
Result<std::unique_ptr<GlobalIndexer>> CreateGlobalIndexer(const std::string&
index_type,
const CoreOptions&
core_options) {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<GlobalIndexer> indexer,
@@ -342,22 +364,10 @@ Result<std::shared_ptr<CommitMessage>>
GlobalIndexWriteTask::WriteIndex(
std::shared_ptr<MemoryPool> pool = memory_pool ? memory_pool :
GetDefaultPool();
std::shared_ptr<arrow::MemoryPool> arrow_pool = GetArrowPool(pool);
- // load schema
- PAIMON_ASSIGN_OR_RAISE(CoreOptions tmp_options,
CoreOptions::FromMap(options, file_system));
- SchemaManager schema_manager(tmp_options.GetFileSystem(), table_path);
- PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>>
latest_table_schema,
- schema_manager.Latest());
- if (latest_table_schema == std::nullopt) {
- return Status::Invalid("not found latest schema");
- }
- // merge options
- const auto& table_schema = latest_table_schema.value();
- auto final_options = table_schema->Options();
- for (const auto& [key, value] : options) {
- final_options[key] = value;
- }
- PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
- CoreOptions::FromMap(final_options, file_system));
+ std::pair<std::shared_ptr<TableSchema>, CoreOptions> schema_and_options;
+ PAIMON_ASSIGN_OR_RAISE(schema_and_options,
+ LoadSchemaAndOptions(table_path, options,
file_system));
+ const auto& [table_schema, core_options] = schema_and_options;
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<GlobalIndexer> indexer,
CreateGlobalIndexer(index_type, core_options));
PAIMON_ASSIGN_OR_RAISE(std::optional<std::vector<std::string>>
extra_field_names,
@@ -410,4 +420,22 @@ Result<std::shared_ptr<CommitMessage>>
GlobalIndexWriteTask::WriteIndex(
extra_field_ids);
}
+Status GlobalIndexWriteTask::DeleteCheckpoint(const std::string& table_path,
+ const std::string& task_id,
+ const std::map<std::string,
std::string>& options,
+ const
std::shared_ptr<FileSystem>& file_system) {
+ PAIMON_ASSIGN_OR_RAISE(std::string task_directory,
+
IndexCheckpointPathFactory::GetTaskDirectory(table_path, task_id));
+ std::pair<std::shared_ptr<TableSchema>, CoreOptions> schema_and_options;
+ PAIMON_ASSIGN_OR_RAISE(schema_and_options,
+ LoadSchemaAndOptions(table_path, options,
file_system));
+ std::shared_ptr<FileSystem> fs = schema_and_options.second.GetFileSystem();
+ PAIMON_ASSIGN_OR_RAISE(bool exists, fs->Exists(task_directory));
+ if (!exists) {
+ return Status::OK();
+ }
+ Status status = fs->Delete(task_directory, /*recursive=*/true);
+ return status.IsNotExist() ? Status::OK() : status;
+}
+
} // namespace paimon
diff --git a/src/paimon/core/index/index_checkpoint_path_factory.h
b/src/paimon/core/index/index_checkpoint_path_factory.h
index 0c1b13e5..0bfa56aa 100644
--- a/src/paimon/core/index/index_checkpoint_path_factory.h
+++ b/src/paimon/core/index/index_checkpoint_path_factory.h
@@ -22,6 +22,10 @@
#include <optional>
#include <string>
+#include "paimon/common/utils/path_util.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
namespace paimon {
/// Path factory for global index checkpoints scoped to one task.
@@ -38,6 +42,13 @@ class IndexCheckpointPathFactory {
/// Returns the checkpoint id when the file name belongs to this factory.
virtual std::optional<int64_t> GetCheckpointId(const std::string&
file_name) const = 0;
+
+ /// Returns the task's checkpoint directory after validating its
identifier.
+ static Result<std::string> GetTaskDirectory(const std::string& table_path,
+ const std::string& task_id) {
+ PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint
task id", task_id));
+ return PathUtil::JoinPath(PathUtil::JoinPath(table_path,
"index/checkpoint"), task_id);
+ }
};
} // namespace paimon
diff --git a/src/paimon/core/utils/file_store_path_factory.cpp
b/src/paimon/core/utils/file_store_path_factory.cpp
index ac15ad63..9acb6d4e 100644
--- a/src/paimon/core/utils/file_store_path_factory.cpp
+++ b/src/paimon/core/utils/file_store_path_factory.cpp
@@ -248,10 +248,10 @@
FileStorePathFactory::CreateGlobalIndexCheckpointPathFactory(const std::string&
};
PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint index
type", index_type));
PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint
field", field_name));
- PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint task
id", task_id));
- std::string directory = PathUtil::JoinPath(IndexPath(root_), "checkpoint");
- std::string file_name_prefix = fmt::format("{}-global-index-{}-{}-{}-{}-",
index_type,
- field_name, range.from,
range.to, task_id);
+ PAIMON_ASSIGN_OR_RAISE(std::string directory,
+ IndexCheckpointPathFactory::GetTaskDirectory(root_,
task_id));
+ std::string file_name_prefix =
+ fmt::format("{}-global-index-{}-{}-{}-", index_type, field_name,
range.from, range.to);
return std::unique_ptr<IndexCheckpointPathFactory>(
std::make_unique<IndexCheckpointPathFactoryImpl>(directory,
file_name_prefix));
}
diff --git a/src/paimon/core/utils/file_store_path_factory_test.cpp
b/src/paimon/core/utils/file_store_path_factory_test.cpp
index bfb2b962..447cb4f4 100644
--- a/src/paimon/core/utils/file_store_path_factory_test.cpp
+++ b/src/paimon/core/utils/file_store_path_factory_test.cpp
@@ -596,11 +596,14 @@ TEST_F(FileStorePathFactoryTest,
TestCreateGlobalIndexCheckpointPathFactory) {
std::shared_ptr<FileStorePathFactory> file_store_path_factory =
CreateFactory(dir->Str());
std::shared_ptr<FileSystem> file_system = dir->GetFileSystem();
- std::string prefix = "lumina-global-index-vector-10-20-task-1-";
+ std::string prefix = "lumina-global-index-vector-10-20-";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<IndexCheckpointPathFactory>
checkpoint_path_factory,
file_store_path_factory->CreateGlobalIndexCheckpointPathFactory(
"lumina", "vector", Range(10, 20), "task-1"));
- std::string checkpoint_dir = PathUtil::JoinPath(dir->Str(),
"index/checkpoint");
+ std::string checkpoint_dir = PathUtil::JoinPath(dir->Str(),
"index/checkpoint/task-1");
+ ASSERT_OK_AND_ASSIGN(std::string task_directory,
+
IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(), "task-1"));
+ ASSERT_EQ(task_directory, checkpoint_dir);
ASSERT_EQ(checkpoint_path_factory->GetDirectoryPath(), checkpoint_dir);
std::string first_path = checkpoint_path_factory->NewPath(0);
ASSERT_EQ(first_path, PathUtil::JoinPath(checkpoint_dir, prefix +
"0.index.ckpt"));
@@ -609,9 +612,9 @@ TEST_F(FileStorePathFactoryTest,
TestCreateGlobalIndexCheckpointPathFactory) {
ASSERT_EQ(checkpoint_path_factory->ToPath("checkpoint"),
PathUtil::JoinPath(checkpoint_dir, "checkpoint"));
ASSERT_EQ(checkpoint_path_factory->GetCheckpointId(prefix +
"9.index.ckpt"), 9);
- ASSERT_EQ(checkpoint_path_factory->GetCheckpointId(
- "lumina-global-index-other-10-20-task-1-9.index.ckpt"),
- std::nullopt);
+ ASSERT_EQ(
+
checkpoint_path_factory->GetCheckpointId("lumina-global-index-other-10-20-9.index.ckpt"),
+ std::nullopt);
ASSERT_EQ(checkpoint_path_factory->GetCheckpointId(prefix +
"invalid-10.index.ckpt"),
std::nullopt);
ASSERT_EQ(checkpoint_path_factory->GetCheckpointId(prefix +
"-1.index.ckpt"), std::nullopt);
@@ -659,14 +662,18 @@ TEST_F(FileStorePathFactoryTest,
TestCheckpointTaskIsolation) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<IndexCheckpointPathFactory>
other_factory,
factory->CreateGlobalIndexCheckpointPathFactory("lumina", "vector",
Range(10, 20), "task-2"));
- ASSERT_EQ(own_factory->GetDirectoryPath(),
other_factory->GetDirectoryPath());
+ ASSERT_NE(own_factory->GetDirectoryPath(),
other_factory->GetDirectoryPath());
ASSERT_OK(fs->Mkdirs(own_factory->GetDirectoryPath()));
+ ASSERT_OK(fs->Mkdirs(other_factory->GetDirectoryPath()));
std::string own_path = own_factory->NewPath(0);
ASSERT_OK(fs->WriteFile(own_path, "checkpoint", /*overwrite=*/false));
+ std::string same_name_other_path = other_factory->NewPath(0);
+ ASSERT_EQ(PathUtil::GetName(own_path),
PathUtil::GetName(same_name_other_path));
+ ASSERT_OK(fs->WriteFile(same_name_other_path, "foreign",
/*overwrite=*/false));
std::string other_path = other_factory->NewPath(100);
ASSERT_OK(fs->WriteFile(other_path, "foreign", /*overwrite=*/false));
ASSERT_EQ(own_factory->GetCheckpointId(PathUtil::GetName(own_path)), 0);
- ASSERT_EQ(own_factory->GetCheckpointId(PathUtil::GetName(other_path)),
std::nullopt);
+ ASSERT_EQ(own_factory->GetCheckpointId(PathUtil::GetName(other_path)),
100);
GlobalIndexFileManager manager(fs, factory->CreateGlobalIndexFileFactory(),
std::move(own_factory));
@@ -690,6 +697,8 @@ TEST_F(FileStorePathFactoryTest,
TestCheckpointTaskIsolation) {
ASSERT_OK(manager.DeleteCheckpoint());
ASSERT_OK_AND_ASSIGN(exists, fs->Exists(other_path));
ASSERT_TRUE(exists);
+ ASSERT_OK_AND_ASSIGN(exists, fs->Exists(same_name_other_path));
+ ASSERT_TRUE(exists);
}
TEST_F(FileStorePathFactoryTest, TestCheckpointRejectsInvalidPathComponents) {
@@ -698,6 +707,7 @@ TEST_F(FileStorePathFactoryTest,
TestCheckpointRejectsInvalidPathComponents) {
auto factory = CreateFactory(dir->Str());
for (const std::string& invalid : std::vector<std::string>{"", ".", "..",
" ", "a/b", "a\\b",
"a\nb",
std::string("a\0b", 3)}) {
+ ASSERT_NOK(IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(),
invalid));
ASSERT_NOK(factory->CreateGlobalIndexCheckpointPathFactory(invalid,
"vector", Range(10, 20),
"task"));
ASSERT_NOK(factory->CreateGlobalIndexCheckpointPathFactory("lumina",
invalid, Range(10, 20),
diff --git a/src/paimon/indexer/lumina/lumina_global_index_test.cpp
b/src/paimon/indexer/lumina/lumina_global_index_test.cpp
index e3451c44..0d5605f4 100644
--- a/src/paimon/indexer/lumina/lumina_global_index_test.cpp
+++ b/src/paimon/indexer/lumina/lumina_global_index_test.cpp
@@ -429,7 +429,7 @@ TEST_F(LuminaGlobalIndexTest, TestCheckpointFileManagement)
{
auto test_root_dir = paimon::test::UniqueTestDirectory::Create();
ASSERT_TRUE(test_root_dir);
std::string test_root = test_root_dir->Str();
- std::string prefix = "lumina-global-index-f0-10-20-task-1-";
+ std::string prefix = "lumina-global-index-f0-10-20-";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<IndexCheckpointPathFactory>
checkpoint_path_factory,
CreateCheckpointPathFactory(test_root, "lumina",
"f0", Range(10, 20)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<FileStorePathFactory> path_factory,
@@ -457,13 +457,13 @@ TEST_F(LuminaGlobalIndexTest,
TestCheckpointFileManagement) {
ASSERT_EQ(second_written, 6);
ASSERT_OK(second_output->Close());
- std::string checkpoint_dir = PathUtil::JoinPath(test_root,
"index/checkpoint");
+ std::string checkpoint_dir = PathUtil::JoinPath(test_root,
"index/checkpoint/task-1");
ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(checkpoint_dir, prefix +
"9.index.ckpt"), "id-nine",
/*overwrite=*/false));
ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(checkpoint_dir, prefix +
"10.index.ckpt"), "id-ten",
/*overwrite=*/false));
std::string unrelated_file =
- PathUtil::JoinPath(checkpoint_dir,
"lumina-global-index-other-10-20-task-2-99.index.ckpt");
+ PathUtil::JoinPath(checkpoint_dir,
"lumina-global-index-other-10-20-99.index.ckpt");
ASSERT_OK(fs_->WriteFile(unrelated_file, "unrelated",
/*overwrite=*/false));
std::string malformed_file =
PathUtil::JoinPath(checkpoint_dir, prefix + "invalid-99.index.ckpt");
@@ -709,7 +709,8 @@ TEST_F(LuminaGlobalIndexTest,
TestWriteAndReadWithTagFilter) {
GlobalIndexIOMeta meta,
WriteGlobalIndex(test_root, tag_data_type, tag_options, tag_array,
Range(0, 3)));
std::vector<BasicFileStatus> checkpoint_files;
- ASSERT_OK(fs_->ListDir(PathUtil::JoinPath(test_root, "index/checkpoint"),
&checkpoint_files));
+ ASSERT_OK(
+ fs_->ListDir(PathUtil::JoinPath(test_root, "index/checkpoint/task-1"),
&checkpoint_files));
ASSERT_TRUE(checkpoint_files.empty());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexReader> reader,
CreateGlobalIndexReader(test_root, data_type_,
tag_options, meta));
diff --git a/test/inte/global_index_test.cpp b/test/inte/global_index_test.cpp
index 97b16982..c5b42162 100644
--- a/test/inte/global_index_test.cpp
+++ b/test/inte/global_index_test.cpp
@@ -576,10 +576,12 @@ TEST_P(GlobalIndexTest,
TestWriteLuminaIndexWithCheckpoint) {
std::shared_ptr<FileSystem> checkpoint_fs = dir_->GetFileSystem();
std::string task_id = "task-1";
std::string checkpoint_dir = PathUtil::JoinPath(table_path,
"index/checkpoint");
- ASSERT_OK(checkpoint_fs->Mkdirs(checkpoint_dir));
+ std::string task_checkpoint_dir = PathUtil::JoinPath(checkpoint_dir,
task_id);
+ ASSERT_OK(checkpoint_fs->Mkdirs(task_checkpoint_dir));
+ ASSERT_OK(checkpoint_fs->Mkdirs(PathUtil::JoinPath(checkpoint_dir,
"another-task")));
auto checkpoint_path = [&](const std::string& task, int64_t id) {
- return PathUtil::JoinPath(
- checkpoint_dir,
fmt::format("lumina-global-index-f1-0-3-{}-{}.index.ckpt", task, id));
+ return PathUtil::JoinPath(PathUtil::JoinPath(checkpoint_dir, task),
+
fmt::format("lumina-global-index-f1-0-3-{}.index.ckpt", id));
};
std::string matching_checkpoint = checkpoint_path(task_id, 9);
ASSERT_OK(checkpoint_fs->WriteFile(matching_checkpoint, "invalid
checkpoint",
@@ -593,10 +595,17 @@ TEST_P(GlobalIndexTest,
TestWriteLuminaIndexWithCheckpoint) {
std::make_shared<IndexedSplitImpl>(split, std::vector<Range>{Range(0,
3)}), lumina_options,
task_id, pool_));
std::vector<BasicFileStatus> checkpoint_files;
- ASSERT_OK(checkpoint_fs->ListDir(checkpoint_dir, &checkpoint_files));
- ASSERT_EQ(checkpoint_files.size(), 1);
- ASSERT_EQ(PathUtil::GetName(checkpoint_files[0].GetPath()),
- PathUtil::GetName(unrelated_checkpoint));
+ ASSERT_OK(checkpoint_fs->ListDir(task_checkpoint_dir, &checkpoint_files));
+ ASSERT_TRUE(checkpoint_files.empty());
+ ASSERT_OK_AND_ASSIGN(bool unrelated_exists,
checkpoint_fs->Exists(unrelated_checkpoint));
+ ASSERT_TRUE(unrelated_exists);
+
+ ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(table_path,
"another-task", lumina_options));
+ ASSERT_OK_AND_ASSIGN(unrelated_exists,
checkpoint_fs->Exists(unrelated_checkpoint));
+ ASSERT_FALSE(unrelated_exists);
+ ASSERT_OK_AND_ASSIGN(bool unrelated_directory_exists,
+
checkpoint_fs->Exists(PathUtil::JoinPath(checkpoint_dir, "another-task")));
+ ASSERT_FALSE(unrelated_directory_exists);
}
TEST_P(GlobalIndexTest, TestWriteLuminaIndexWithMismatchedDimension) {