fix: recovery from crash during optimization (#246)
This commit is contained in:
parent
209ef1fba8
commit
ae345ad070
|
|
@ -187,7 +187,7 @@ class BufferStorage : public IndexStorage {
|
|||
}
|
||||
|
||||
//! Open storage
|
||||
int open(const std::string &path, bool /*create*/) override {
|
||||
int open(const std::string &path, bool /*create_if_missing*/) override {
|
||||
file_name_ = path;
|
||||
buffer_pool_ = std::make_shared<ailego::VecBufferPool>(path);
|
||||
buffer_pool_handle_ = std::make_shared<ailego::VecBufferPoolHandle>(
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@ class MMapFileStorage : public IndexStorage {
|
|||
}
|
||||
|
||||
//! Open storage
|
||||
int open(const std::string &path, bool create) override {
|
||||
if (!ailego::File::IsExist(path) && create) {
|
||||
int open(const std::string &path, bool create_if_missing) override {
|
||||
if (!ailego::File::IsExist(path) && create_if_missing) {
|
||||
size_t last_slash = path.rfind('/');
|
||||
if (last_slash != std::string::npos) {
|
||||
ailego::File::MakePath(path.substr(0, last_slash));
|
||||
|
|
|
|||
|
|
@ -958,7 +958,7 @@ std::vector<SegmentTask::Ptr> CollectionImpl::build_compact_task(
|
|||
if (current_actual_doc_count + actual_doc_count >
|
||||
max_doc_count_per_segment) {
|
||||
// only create SegmentCompactTask when rebuild=true
|
||||
task = SegmentTask::CreateComapctTask(
|
||||
task = SegmentTask::CreateCompactTask(
|
||||
CompactTask{path_, schema, current_group,
|
||||
allocate_segment_id_for_tmp_segment(), filter,
|
||||
!options_.enable_mmap_, concurrency});
|
||||
|
|
@ -972,7 +972,7 @@ std::vector<SegmentTask::Ptr> CollectionImpl::build_compact_task(
|
|||
current_group[0], "", nullptr, concurrency});
|
||||
skip_task = current_group[0]->all_vector_index_ready();
|
||||
} else {
|
||||
task = SegmentTask::CreateComapctTask(
|
||||
task = SegmentTask::CreateCompactTask(
|
||||
CompactTask{path_, schema, current_group,
|
||||
allocate_segment_id_for_tmp_segment(), nullptr,
|
||||
!options_.enable_mmap_, concurrency});
|
||||
|
|
@ -1001,7 +1001,7 @@ std::vector<SegmentTask::Ptr> CollectionImpl::build_compact_task(
|
|||
task = SegmentTask::CreateCreateVectorIndexTask(
|
||||
CreateVectorIndexTask{current_group[0], "", nullptr, concurrency});
|
||||
} else {
|
||||
task = SegmentTask::CreateComapctTask(CompactTask{
|
||||
task = SegmentTask::CreateCompactTask(CompactTask{
|
||||
path_, schema, current_group, allocate_segment_id_for_tmp_segment(),
|
||||
rebuild ? filter : nullptr, !options_.enable_mmap_, concurrency});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1154,7 +1154,7 @@ struct Doc::ValueEqual {
|
|||
const std::vector<float> &b) const {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
if (std::fabs(a[i] - b[i]) >= 1e-6f) return false;
|
||||
if (std::fabs(a[i] - b[i]) >= 1e-4f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1162,7 +1162,7 @@ struct Doc::ValueEqual {
|
|||
const std::vector<double> &b) const {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
if (std::fabs(a[i] - b[i]) >= 1e-9) return false;
|
||||
if (std::fabs(a[i] - b[i]) >= 1e-6) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ class SegmentImpl : public Segment,
|
|||
const std::vector<int> &indices) const override;
|
||||
|
||||
ExecBatchPtr fetch(const std::vector<std::string> &columns,
|
||||
int indice) const override;
|
||||
int index) const override;
|
||||
|
||||
RecordBatchReaderPtr scan(
|
||||
const std::vector<std::string> &columns) const override;
|
||||
|
|
@ -1631,6 +1631,13 @@ Status SegmentImpl::create_vector_index(
|
|||
|
||||
std::string index_file_path = FileHelper::MakeVectorIndexPath(
|
||||
path_, column, segment_meta_->id(), block_id);
|
||||
if (FileHelper::FileExists(index_file_path)) {
|
||||
LOG_WARN(
|
||||
"Index file[%s] already exists (possible crash residue); cleaning "
|
||||
"and overwriting.",
|
||||
index_file_path.c_str());
|
||||
FileHelper::RemoveFile(index_file_path);
|
||||
}
|
||||
auto vector_indexer = merge_vector_indexer(
|
||||
index_file_path, column, *field_with_new_index_params, concurrency);
|
||||
if (!vector_indexer.has_value()) {
|
||||
|
|
@ -1668,6 +1675,13 @@ Status SegmentImpl::create_vector_index(
|
|||
|
||||
std::string index_file_path = FileHelper::MakeVectorIndexPath(
|
||||
path_, column, segment_meta_->id(), block_id);
|
||||
if (FileHelper::FileExists(index_file_path)) {
|
||||
LOG_WARN(
|
||||
"Index file[%s] already exists (possible crash residue); cleaning "
|
||||
"and overwriting.",
|
||||
index_file_path.c_str());
|
||||
FileHelper::RemoveFile(index_file_path);
|
||||
}
|
||||
auto vector_indexer = merge_vector_indexer(index_file_path, column,
|
||||
*field_with_flat, concurrency);
|
||||
if (!vector_indexer.has_value()) {
|
||||
|
|
@ -1700,6 +1714,13 @@ Status SegmentImpl::create_vector_index(
|
|||
|
||||
std::string index_file_path = FileHelper::MakeQuantizeVectorIndexPath(
|
||||
path_, column, segment_meta_->id(), quant_block_id);
|
||||
if (FileHelper::FileExists(index_file_path)) {
|
||||
LOG_WARN(
|
||||
"Index file[%s] already exists (possible crash residue); cleaning "
|
||||
"and overwriting.",
|
||||
index_file_path.c_str());
|
||||
FileHelper::RemoveFile(index_file_path);
|
||||
}
|
||||
auto vector_indexer = merge_vector_indexer(
|
||||
index_file_path, column, *field_with_new_index_params, concurrency);
|
||||
if (!vector_indexer.has_value()) {
|
||||
|
|
@ -1772,6 +1793,13 @@ Status SegmentImpl::create_vector_index(
|
|||
|
||||
std::string index_file_path = FileHelper::MakeQuantizeVectorIndexPath(
|
||||
path_, column, segment_meta_->id(), quant_block_id);
|
||||
if (FileHelper::FileExists(index_file_path)) {
|
||||
LOG_WARN(
|
||||
"Index file[%s] already exists (possible crash residue); cleaning "
|
||||
"and overwriting.",
|
||||
index_file_path.c_str());
|
||||
FileHelper::RemoveFile(index_file_path);
|
||||
}
|
||||
auto vector_indexer = merge_vector_indexer(
|
||||
index_file_path, column, *field_with_new_index_params, concurrency);
|
||||
if (!vector_indexer.has_value()) {
|
||||
|
|
@ -2031,7 +2059,7 @@ Status SegmentImpl::create_scalar_index(const std::vector<std::string> &columns,
|
|||
s = SegmentHelper::ReduceScalarIndex(new_scalar_indexer, batch_value,
|
||||
accu_doc_count);
|
||||
if (!s.ok()) {
|
||||
LOG_ERROR("Reduce Scalar Index faield, err: %s", s.message().c_str());
|
||||
LOG_ERROR("Reduce Scalar Index failed, err: %s", s.message().c_str());
|
||||
}
|
||||
CHECK_RETURN_STATUS(s);
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class Segment {
|
|||
virtual Status drop_column(const std::string &column_name) = 0;
|
||||
|
||||
virtual Status create_all_vector_index(
|
||||
int concurrency, SegmentMeta::Ptr *new_segmnet_meta,
|
||||
int concurrency, SegmentMeta::Ptr *new_segment_meta,
|
||||
std::unordered_map<std::string, VectorColumnIndexer::Ptr>
|
||||
*vector_indexers,
|
||||
std::unordered_map<std::string, VectorColumnIndexer::Ptr>
|
||||
|
|
@ -86,14 +86,14 @@ class Segment {
|
|||
// defined in segment.h cause it needs to access block_id generator
|
||||
virtual Status create_vector_index(
|
||||
const std::string &column, const IndexParams::Ptr &index_params,
|
||||
int concurrency, SegmentMeta::Ptr *new_segmnet_meta,
|
||||
int concurrency, SegmentMeta::Ptr *new_segment_meta,
|
||||
std::unordered_map<std::string, VectorColumnIndexer::Ptr>
|
||||
*vector_indexers,
|
||||
std::unordered_map<std::string, VectorColumnIndexer::Ptr>
|
||||
*quant_vector_indexers) = 0;
|
||||
|
||||
virtual Status drop_vector_index(
|
||||
const std::string &column, SegmentMeta::Ptr *new_segmnet_meta,
|
||||
const std::string &column, SegmentMeta::Ptr *new_segment_meta,
|
||||
std::unordered_map<std::string, VectorColumnIndexer::Ptr>
|
||||
*vector_indexers) = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class SegmentTask {
|
|||
std::variant<CompactTask, CreateVectorIndexTask, DropVectorIndexTask,
|
||||
CreateScalarIndexTask, DropScalarIndexTask>;
|
||||
|
||||
static Ptr CreateComapctTask(const CompactTask &task) {
|
||||
static Ptr CreateCompactTask(const CompactTask &task) {
|
||||
return std::make_shared<SegmentTask>(task);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ class IndexStorage : public IndexModule {
|
|||
virtual int cleanup(void) = 0;
|
||||
|
||||
//! Open storage
|
||||
virtual int open(const std::string &path, bool create) = 0;
|
||||
virtual int open(const std::string &path, bool create_if_missing) = 0;
|
||||
|
||||
//! Flush storage
|
||||
virtual int flush(void) = 0;
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ if(APPLE)
|
|||
endif()
|
||||
|
||||
|
||||
# Build data_generator executable
|
||||
cc_binary(
|
||||
NAME data_generator
|
||||
LIBS zvec_db
|
||||
# Common libraries
|
||||
set(CRASH_RECOVERY_COMMON_LIBS
|
||||
zvec_db
|
||||
zvec_proto
|
||||
core_knn_flat
|
||||
core_knn_flat_sparse
|
||||
|
|
@ -34,36 +33,41 @@ cc_binary(
|
|||
core_quantizer
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
|
||||
|
||||
# Build data_generator executable
|
||||
cc_binary(
|
||||
NAME data_generator
|
||||
LIBS ${CRASH_RECOVERY_COMMON_LIBS}
|
||||
SRCS data_generator.cc
|
||||
INCS .. ../../src
|
||||
LDFLAGS ${APPLE_FRAMEWORK_LIBS}
|
||||
)
|
||||
|
||||
|
||||
# Build collection_optimizer executable
|
||||
cc_binary(
|
||||
NAME collection_optimizer
|
||||
LIBS ${CRASH_RECOVERY_COMMON_LIBS}
|
||||
SRCS collection_optimizer.cc
|
||||
INCS .. ../../src
|
||||
LDFLAGS ${APPLE_FRAMEWORK_LIBS}
|
||||
)
|
||||
|
||||
|
||||
# Build test executables
|
||||
file(GLOB ALL_TEST_SRCS *_test.cc)
|
||||
foreach(CC_SRCS ${ALL_TEST_SRCS})
|
||||
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
|
||||
cc_gmock(
|
||||
NAME ${CC_TARGET} STRICT
|
||||
LIBS zvec_db
|
||||
zvec_proto
|
||||
core_knn_flat
|
||||
core_knn_flat_sparse
|
||||
core_knn_hnsw
|
||||
core_knn_hnsw_sparse
|
||||
core_knn_ivf
|
||||
core_knn_hnsw_rabitq
|
||||
core_mix_reducer
|
||||
core_metric
|
||||
core_utility
|
||||
core_quantizer
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${CMAKE_DL_LIBS}
|
||||
LIBS ${CRASH_RECOVERY_COMMON_LIBS}
|
||||
SRCS ${CC_SRCS}
|
||||
INCS .. ../../src
|
||||
LDFLAGS ${APPLE_FRAMEWORK_LIBS}
|
||||
)
|
||||
add_dependencies(${CC_TARGET} data_generator)
|
||||
add_dependencies(${CC_TARGET} collection_optimizer)
|
||||
cc_test_suite(zvec_crash_recovery ${CC_TARGET})
|
||||
endforeach()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2025-present the zvec project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
#include <unistd.h>
|
||||
#include <filesystem>
|
||||
#include <zvec/db/collection.h>
|
||||
#include <zvec/db/options.h>
|
||||
#include "zvec/ailego/logger/logger.h"
|
||||
|
||||
|
||||
struct Config {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
|
||||
bool ParseArgs(int argc, char **argv, Config &config) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--path" && i + 1 < argc) {
|
||||
config.path = argv[++i];
|
||||
} else if (arg == "--help" || arg == "-h") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if (config.path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void PrintUsage(const char *program) {
|
||||
std::cout << "Usage: " << program << " --path <collection_path>" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Arguments:" << std::endl;
|
||||
std::cout << " --path Path to the collection (required)"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
Config config;
|
||||
|
||||
// Parse arguments
|
||||
if (!ParseArgs(argc, argv, config)) {
|
||||
PrintUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
std::filesystem::path cwd = std::filesystem::current_path();
|
||||
std::cout << "[collection_optimizer] Current Working Directory: "
|
||||
<< cwd.string() << std::endl;
|
||||
} catch (const std::filesystem::filesystem_error &e) {
|
||||
std::cout << "[collection_optimizer] Failed to get the current working "
|
||||
"directory: "
|
||||
<< e.what() << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "Configuration:" << std::endl;
|
||||
std::cout << " Path: " << config.path << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
auto result =
|
||||
zvec::Collection::Open(config.path, zvec::CollectionOptions{false, true});
|
||||
if (!result) {
|
||||
LOG_ERROR("Failed to open collection[%s]: %s", config.path.c_str(),
|
||||
result.error().c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto collection = result.value();
|
||||
std::cout << "Collection[" << config.path.c_str() << "] opened successfully"
|
||||
<< std::endl;
|
||||
|
||||
// Print initial stats
|
||||
std::cout << "Initial stats: " << collection->Stats()->to_string_formatted()
|
||||
<< std::endl;
|
||||
|
||||
auto s = collection->Optimize();
|
||||
if (s.ok()) {
|
||||
std::cout << "Optimize completed successfully" << std::endl;
|
||||
// Print final stats
|
||||
std::cout << "Final stats: " << collection->Stats()->to_string_formatted() << std::endl;
|
||||
return 0;
|
||||
} else {
|
||||
std::cout << "Optimize failed: " << s.message() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright 2025-present the zvec project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <gtest/gtest.h>
|
||||
#include <zvec/db/collection.h>
|
||||
#include <zvec/db/doc.h>
|
||||
#include <zvec/db/schema.h>
|
||||
#include "utility.h"
|
||||
|
||||
|
||||
namespace zvec {
|
||||
|
||||
|
||||
static std::string optimizer_bin_;
|
||||
const std::string collection_name_{"optimize_recovery_test"};
|
||||
const std::string dir_path_{"optimize_recovery_test_db"};
|
||||
const zvec::CollectionOptions options_{false, true, 256 * 1024};
|
||||
const int batch_size{50};
|
||||
const int num_batches{1000};
|
||||
|
||||
|
||||
static std::string LocateOptimizeGenerator() {
|
||||
namespace fs = std::filesystem;
|
||||
const std::vector<std::string> candidates{"./collection_optimizer",
|
||||
"./bin/collection_optimizer"};
|
||||
for (const auto &p : candidates) {
|
||||
if (fs::exists(p)) {
|
||||
return fs::canonical(p).string();
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("collection_optimizer binary not found");
|
||||
}
|
||||
|
||||
|
||||
void RunOptimizer(const std::string &path) {
|
||||
pid_t pid = fork();
|
||||
ASSERT_GE(pid, 0);
|
||||
|
||||
if (pid == 0) { // Child process
|
||||
char arg_path[] = "--path";
|
||||
char *args[] = {const_cast<char *>(optimizer_bin_.c_str()), arg_path,
|
||||
const_cast<char *>(path.c_str()), nullptr};
|
||||
execvp(args[0], args);
|
||||
perror("execvp failed");
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
ASSERT_TRUE(WIFEXITED(status))
|
||||
<< "Child process did not exit normally. Terminated by signal?";
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
ASSERT_EQ(exit_code, 0) << "optimizer failed with exit code: " << exit_code;
|
||||
}
|
||||
|
||||
|
||||
void RunOptimizerAndCrash(const std::string &path, int seconds) {
|
||||
pid_t pid = fork();
|
||||
ASSERT_GE(pid, 0);
|
||||
|
||||
if (pid == 0) { // Child process
|
||||
char arg_path[] = "--path";
|
||||
char *args[] = {const_cast<char *>(optimizer_bin_.c_str()), arg_path,
|
||||
const_cast<char *>(path.c_str()), nullptr};
|
||||
execvp(args[0], args);
|
||||
perror("execvp failed");
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(seconds));
|
||||
if (kill(pid, 0) == 0) {
|
||||
kill(pid, SIGKILL);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
ASSERT_TRUE(WIFSIGNALED(status))
|
||||
<< "Child process was not killed by a signal. It exited normally?";
|
||||
}
|
||||
|
||||
|
||||
class OptimizeRecoveryTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
system("rm -rf ./optimize_recovery_test_db");
|
||||
ASSERT_NO_THROW(optimizer_bin_ = LocateOptimizeGenerator());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
system("rm -rf ./optimize_recovery_test_db");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TEST_F(OptimizeRecoveryTest, CrashDuringOptimize) {
|
||||
{ // Create a collection and insert some documents
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
auto collection = result.value();
|
||||
|
||||
for (int batch = 0; batch < num_batches; batch++) {
|
||||
std::vector<Doc> docs;
|
||||
for (int i = 0; i < batch_size; i++) {
|
||||
docs.push_back(CreateTestDoc(batch * batch_size + i, 0));
|
||||
}
|
||||
auto write_result = collection->Insert(docs);
|
||||
ASSERT_TRUE(write_result);
|
||||
for (auto &s : write_result.value()) {
|
||||
ASSERT_TRUE(s.ok());
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(collection->Stats()->doc_count, num_batches * batch_size);
|
||||
collection.reset();
|
||||
}
|
||||
|
||||
RunOptimizerAndCrash(dir_path_, 4);
|
||||
|
||||
{ // Open the collection and verify data integrity
|
||||
auto result = Collection::Open(dir_path_, options_);
|
||||
ASSERT_TRUE(result.has_value())
|
||||
<< "Failed to reopen collection after crash. "
|
||||
"Recovery mechanism may be broken.";
|
||||
auto collection = result.value();
|
||||
uint64_t doc_count{collection->Stats().value().doc_count};
|
||||
ASSERT_EQ(doc_count, num_batches * batch_size);
|
||||
for (uint64_t doc_id = 0; doc_id < doc_count; doc_id++) {
|
||||
Doc expected_doc = CreateTestDoc(doc_id, 0);
|
||||
std::vector<std::string> pks{};
|
||||
pks.emplace_back(expected_doc.pk());
|
||||
if (auto res = collection->Fetch(pks); res) {
|
||||
auto map = res.value();
|
||||
if (map.find(expected_doc.pk()) == map.end()) {
|
||||
FAIL() << "Returned map does not contain doc[" << expected_doc.pk()
|
||||
<< "]";
|
||||
}
|
||||
const auto actual_doc = map.at(expected_doc.pk());
|
||||
ASSERT_EQ(*actual_doc, expected_doc)
|
||||
<< "Data mismatch for doc[" << expected_doc.pk() << "]";
|
||||
} else {
|
||||
FAIL() << "Failed to fetch doc[" << expected_doc.pk() << "]";
|
||||
}
|
||||
}
|
||||
|
||||
VectorQuery query;
|
||||
query.topk_ = 10;
|
||||
std::vector<float> feature(128, 0.0);
|
||||
query.query_vector_.assign((const char *)feature.data(),
|
||||
feature.size() * sizeof(float));
|
||||
query.field_name_ = "dense_fp32_field";
|
||||
auto query_result = collection->Query(query);
|
||||
ASSERT_TRUE(query_result);
|
||||
auto doc_list = query_result.value();
|
||||
ASSERT_EQ(doc_list.size(), 10);
|
||||
ASSERT_EQ(doc_list[0]->pk(), "pk_0");
|
||||
|
||||
// Insert some more documents
|
||||
for (int batch = num_batches; batch < num_batches + 500; batch++) {
|
||||
std::vector<Doc> docs;
|
||||
for (int i = 0; i < batch_size; i++) {
|
||||
docs.push_back(CreateTestDoc(batch * batch_size + i, 0));
|
||||
}
|
||||
auto write_result = collection->Insert(docs);
|
||||
ASSERT_TRUE(write_result);
|
||||
for (auto &s : write_result.value()) {
|
||||
ASSERT_TRUE(s.ok());
|
||||
}
|
||||
}
|
||||
|
||||
collection.reset();
|
||||
}
|
||||
|
||||
RunOptimizer(dir_path_);
|
||||
|
||||
// Open the collection and verify data integrity
|
||||
auto result = Collection::Open(dir_path_, options_);
|
||||
ASSERT_TRUE(result.has_value()) << "Failed to reopen collection after crash. "
|
||||
"Recovery mechanism may be broken.";
|
||||
auto collection = result.value();
|
||||
uint64_t doc_count{collection->Stats().value().doc_count};
|
||||
ASSERT_EQ(doc_count, (num_batches + 500) * batch_size);
|
||||
for (uint64_t doc_id = 0; doc_id < doc_count; doc_id++) {
|
||||
Doc expected_doc = CreateTestDoc(doc_id, 0);
|
||||
std::vector<std::string> pks{};
|
||||
pks.emplace_back(expected_doc.pk());
|
||||
if (auto res = collection->Fetch(pks); res) {
|
||||
auto map = res.value();
|
||||
if (map.find(expected_doc.pk()) == map.end()) {
|
||||
FAIL() << "Returned map does not contain doc[" << expected_doc.pk()
|
||||
<< "]";
|
||||
}
|
||||
const auto actual_doc = map.at(expected_doc.pk());
|
||||
ASSERT_EQ(*actual_doc, expected_doc)
|
||||
<< "Data mismatch for doc[" << expected_doc.pk() << "]";
|
||||
} else {
|
||||
FAIL() << "Failed to fetch doc[" << expected_doc.pk() << "]";
|
||||
}
|
||||
}
|
||||
|
||||
VectorQuery query;
|
||||
query.topk_ = 10;
|
||||
std::vector<float> feature(128, 0.0);
|
||||
query.query_vector_.assign((const char *)feature.data(),
|
||||
feature.size() * sizeof(float));
|
||||
query.field_name_ = "dense_fp32_field";
|
||||
auto query_result = collection->Query(query);
|
||||
ASSERT_TRUE(query_result);
|
||||
auto doc_list = query_result.value();
|
||||
ASSERT_EQ(doc_list.size(), 10);
|
||||
ASSERT_EQ(doc_list[0]->pk(), "pk_0");
|
||||
}
|
||||
|
||||
|
||||
} // namespace zvec
|
||||
|
|
@ -49,7 +49,7 @@ inline CollectionSchema::Ptr CreateTestSchema(
|
|||
"array_string_field", DataType::ARRAY_STRING, false));
|
||||
schema->add_field(std::make_shared<FieldSchema>(
|
||||
"dense_fp32_field", DataType::VECTOR_FP32, 128, false,
|
||||
std::make_shared<HnswIndexParams>(MetricType::COSINE)));
|
||||
std::make_shared<HnswIndexParams>(MetricType::L2)));
|
||||
schema->add_field(std::make_shared<FieldSchema>(
|
||||
"sparse_fp32_field", DataType::SPARSE_VECTOR_FP32, 0, false,
|
||||
std::make_shared<HnswIndexParams>(MetricType::IP)));
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ namespace zvec {
|
|||
|
||||
|
||||
static std::string data_generator_bin_;
|
||||
const std::string collection_name_{"crash_test"};
|
||||
const std::string dir_path_{"crash_test_db"};
|
||||
const zvec::CollectionOptions options_{false, true};
|
||||
const std::string collection_name_{"write_recovery_test"};
|
||||
const std::string dir_path_{"write_recovery_test_db"};
|
||||
const zvec::CollectionOptions options_{false, true, 256 * 1024};
|
||||
|
||||
|
||||
static std::string LocateDataGenerator() {
|
||||
|
|
@ -126,12 +126,12 @@ void RunGeneratorAndCrash(const std::string &start, const std::string &end,
|
|||
class CrashRecoveryTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
system("rm -rf ./crash_test_db");
|
||||
system("rm -rf ./write_recovery_test_db");
|
||||
ASSERT_NO_THROW(data_generator_bin_ = LocateDataGenerator());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
system("rm -rf ./crash_test_db");
|
||||
system("rm -rf ./write_recovery_test_db");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ TEST_F(CrashRecoveryTest, BasicInsertAndReopen) {
|
|||
{
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
collection.reset();
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringInsertion) {
|
|||
{
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
collection.reset();
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringUpsert) {
|
|||
{
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
collection.reset();
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringUpsert) {
|
|||
RunGenerator("0", "5000", "insert", "0");
|
||||
{
|
||||
auto result = Collection::Open(dir_path_, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
ASSERT_EQ(collection->Stats().value().doc_count, 5000)
|
||||
<< "Document count mismatch";
|
||||
|
|
@ -250,7 +250,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringUpdate) {
|
|||
{
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
collection.reset();
|
||||
}
|
||||
|
|
@ -258,7 +258,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringUpdate) {
|
|||
RunGenerator("0", "18000", "upsert", "0");
|
||||
{
|
||||
auto result = Collection::Open(dir_path_, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
ASSERT_EQ(collection->Stats().value().doc_count, 18000)
|
||||
<< "Document count mismatch";
|
||||
|
|
@ -302,7 +302,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringDelete) {
|
|||
{
|
||||
auto schema = CreateTestSchema(collection_name_);
|
||||
auto result = Collection::CreateAndOpen(dir_path_, *schema, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
collection.reset();
|
||||
}
|
||||
|
|
@ -310,7 +310,7 @@ TEST_F(CrashRecoveryTest, CrashRecoveryDuringDelete) {
|
|||
RunGenerator("0", "18000", "insert", "0");
|
||||
{
|
||||
auto result = Collection::Open(dir_path_, options_);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.has_value()) << result.error().message();
|
||||
auto collection = result.value();
|
||||
ASSERT_EQ(collection->Stats().value().doc_count, 18000)
|
||||
<< "Document count mismatch";
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ TEST_F(SegmentHelperTest, CompactTask_General) {
|
|||
);
|
||||
|
||||
// Create segment task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
|
||||
// Verify task creation
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
|
@ -214,7 +214,7 @@ TEST_F(SegmentHelperTest, CompactTask_ScalarIndex) {
|
|||
);
|
||||
|
||||
// Create segment task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
|
||||
// Verify task creation
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
|
@ -307,7 +307,7 @@ TEST_F(SegmentHelperTest, CompactTask_VectorIndex) {
|
|||
);
|
||||
|
||||
// Create segment task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
|
||||
// Verify task creation
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
|
@ -395,7 +395,7 @@ TEST_F(SegmentHelperTest, CompactTask_MultipleSegments) {
|
|||
);
|
||||
|
||||
// Create segment task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
|
||||
// Verify task creation
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
|
@ -484,7 +484,7 @@ TEST_F(SegmentHelperTest, CompactTask_Filter) {
|
|||
);
|
||||
|
||||
// Create and execute task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
||||
Status status = SegmentHelper::Execute(segment_task);
|
||||
|
|
@ -563,7 +563,7 @@ TEST_F(SegmentHelperTest, CompactTask_FilterAll) {
|
|||
);
|
||||
|
||||
// Create and execute task
|
||||
auto segment_task = SegmentTask::CreateComapctTask(task);
|
||||
auto segment_task = SegmentTask::CreateCompactTask(task);
|
||||
ASSERT_TRUE(segment_task != nullptr);
|
||||
|
||||
Status status = SegmentHelper::Execute(segment_task);
|
||||
|
|
|
|||
Loading…
Reference in New Issue