feat(index): expose is_dirty status through core Index interface (#488)
Adds is_dirty() at each layer: IndexMapping → IndexStorage (virtual) → MMapFileStorage / BufferStorage → Index::IsDirty().
This commit is contained in:
parent
841b1ed3f9
commit
b1af0e4ebf
|
|
@ -381,6 +381,13 @@ int Index::Flush() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool Index::IsDirty() const {
|
||||
if (!storage_) {
|
||||
return false;
|
||||
}
|
||||
return storage_->is_dirty();
|
||||
}
|
||||
|
||||
int Index::Fetch(const uint32_t doc_id, VectorDataBuffer *vector_data_buffer) {
|
||||
if (!is_open_) {
|
||||
LOG_ERROR("Index is not open");
|
||||
|
|
|
|||
|
|
@ -823,6 +823,7 @@ class BufferStorage : public IndexStorage {
|
|||
size_t capacity = static_cast<size_t>(meta->padding_size + meta->data_size);
|
||||
memcpy(segment->data(), IndexVersion::Details(), data_size);
|
||||
segment->set_dirty();
|
||||
set_as_dirty();
|
||||
meta->data_crc = ailego::Crc32c::Hash(segment->data(), data_size, 0);
|
||||
meta->data_size = data_size;
|
||||
meta->padding_size = capacity - data_size;
|
||||
|
|
@ -860,6 +861,10 @@ class BufferStorage : public IndexStorage {
|
|||
return ret;
|
||||
}
|
||||
|
||||
bool is_dirty(void) const override {
|
||||
return index_dirty_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
//! Mark the index as dirty. HOT PATH: store(true) unconditionally --
|
||||
//! a load-then-store guard could let a stale cached `true` skip the
|
||||
//! store after flush_index() CAS'd dirty=false on another core, losing
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ class MMapFileStorage : public IndexStorage {
|
|||
if (data_tail > meta->data_size) {
|
||||
meta->data_size = data_tail;
|
||||
meta->padding_size = capacity_ - data_tail;
|
||||
owner_->set_as_dirty();
|
||||
}
|
||||
owner_->set_as_dirty();
|
||||
memmove((uint8_t *)segment_->data() + offset, data, len);
|
||||
segment_->set_dirty();
|
||||
return len;
|
||||
|
|
@ -254,6 +254,7 @@ class MMapFileStorage : public IndexStorage {
|
|||
size_t capacity = static_cast<size_t>(meta->padding_size + meta->data_size);
|
||||
memcpy(segment->data(), IndexVersion::Details(), data_size);
|
||||
segment->set_dirty();
|
||||
set_as_dirty();
|
||||
meta->data_crc = ailego::Crc32c::Hash(segment->data(), data_size, 0);
|
||||
meta->data_size = data_size;
|
||||
meta->padding_size = capacity - data_size;
|
||||
|
|
@ -285,6 +286,10 @@ class MMapFileStorage : public IndexStorage {
|
|||
return mapping_.huge_page();
|
||||
}
|
||||
|
||||
bool is_dirty(void) const override {
|
||||
return index_dirty_ || mapping_.is_header_dirty();
|
||||
}
|
||||
|
||||
//! Set the index file as dirty
|
||||
void set_as_dirty(void) {
|
||||
index_dirty_ = true;
|
||||
|
|
|
|||
|
|
@ -181,6 +181,11 @@ class IndexMapping {
|
|||
return huge_page_;
|
||||
}
|
||||
|
||||
//! Test if any data needs to be flushed to disk
|
||||
bool is_header_dirty() const {
|
||||
return header_dirty_;
|
||||
}
|
||||
|
||||
protected:
|
||||
//! Initialize index file mapping
|
||||
int init_index_mapping(size_t len);
|
||||
|
|
|
|||
|
|
@ -386,6 +386,11 @@ class IndexStorage : public IndexModule {
|
|||
return MemoryBlock::MBT_MMAP;
|
||||
}
|
||||
|
||||
//! Test if the storage has unflushed data
|
||||
virtual bool is_dirty(void) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
//! Retrieve file ptr if has
|
||||
virtual std::shared_ptr<ailego::File> file(void) const {
|
||||
return nullptr;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ class Index {
|
|||
return is_trained_;
|
||||
}
|
||||
|
||||
bool IsDirty() const;
|
||||
|
||||
uint32_t GetDocCount() const {
|
||||
if (streamer_ == nullptr) {
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -1856,6 +1856,124 @@ TEST(IndexInterface, ContiguousMemoryEndToEnd) {
|
|||
.build());
|
||||
}
|
||||
|
||||
TEST(IndexInterface, IsDirty) {
|
||||
constexpr uint32_t kDimension = 16;
|
||||
const std::string index_name{"test_is_dirty.index"};
|
||||
|
||||
auto test = [&](const BaseIndexParam::Pointer ¶m) {
|
||||
zvec::test_util::RemoveTestFiles(index_name);
|
||||
|
||||
// Before open: not dirty (no storage)
|
||||
{
|
||||
auto index = IndexFactory::CreateAndInitIndex(*param);
|
||||
ASSERT_NE(nullptr, index);
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
}
|
||||
|
||||
// Create the index file: dirty from initial metadata writes
|
||||
{
|
||||
auto index = IndexFactory::CreateAndInitIndex(*param);
|
||||
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
||||
ASSERT_TRUE(index->IsDirty());
|
||||
ASSERT_EQ(0, index->Flush());
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
index->Close();
|
||||
}
|
||||
|
||||
// Reopen existing file: should be clean
|
||||
auto index = IndexFactory::CreateAndInitIndex(*param);
|
||||
index->Open(index_name, {StorageOptions::StorageType::kMMAP, false});
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
|
||||
// Add a vector: should become dirty
|
||||
std::vector<float> vec(kDimension, 1.0f);
|
||||
VectorData vd;
|
||||
vd.vector = DenseVector{vec.data()};
|
||||
ASSERT_EQ(0, index->Add(vd, 1));
|
||||
ASSERT_TRUE(index->IsDirty());
|
||||
|
||||
// Flush: should become clean
|
||||
ASSERT_EQ(0, index->Flush());
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
|
||||
// Add another vector: dirty again
|
||||
ASSERT_EQ(0, index->Add(vd, 2));
|
||||
ASSERT_TRUE(index->IsDirty());
|
||||
|
||||
// Close flushes implicitly, verify no crash
|
||||
index->Close();
|
||||
zvec::test_util::RemoveTestFiles(index_name);
|
||||
};
|
||||
|
||||
test(FlatIndexParamBuilder()
|
||||
.WithMetricType(MetricType::kInnerProduct)
|
||||
.WithDataType(DataType::DT_FP32)
|
||||
.WithDimension(kDimension)
|
||||
.WithIsSparse(false)
|
||||
.Build());
|
||||
|
||||
test(HNSWIndexParamBuilder()
|
||||
.WithMetricType(MetricType::kInnerProduct)
|
||||
.WithDataType(DataType::DT_FP32)
|
||||
.WithDimension(kDimension)
|
||||
.WithIsSparse(false)
|
||||
.WithEFConstruction(100)
|
||||
.Build());
|
||||
}
|
||||
|
||||
TEST(IndexInterface, IsDirtyBufferPool) {
|
||||
constexpr uint32_t kDimension = 16;
|
||||
const std::string index_name{"test_is_dirty_bp.index"};
|
||||
|
||||
zvec::test_util::RemoveTestFiles(index_name);
|
||||
|
||||
// First create and populate the index with MMAP storage
|
||||
{
|
||||
auto param = FlatIndexParamBuilder()
|
||||
.WithMetricType(MetricType::kInnerProduct)
|
||||
.WithDataType(DataType::DT_FP32)
|
||||
.WithDimension(kDimension)
|
||||
.WithIsSparse(false)
|
||||
.Build();
|
||||
auto index = IndexFactory::CreateAndInitIndex(*param);
|
||||
ASSERT_NE(nullptr, index);
|
||||
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
||||
std::vector<float> vec(kDimension, 1.0f);
|
||||
VectorData vd;
|
||||
vd.vector = DenseVector{vec.data()};
|
||||
ASSERT_EQ(0, index->Add(vd, 1));
|
||||
index->Close();
|
||||
}
|
||||
|
||||
// Reopen with BufferPool storage in writable mode
|
||||
{
|
||||
auto param = FlatIndexParamBuilder()
|
||||
.WithMetricType(MetricType::kInnerProduct)
|
||||
.WithDataType(DataType::DT_FP32)
|
||||
.WithDimension(kDimension)
|
||||
.WithIsSparse(false)
|
||||
.Build();
|
||||
auto index = IndexFactory::CreateAndInitIndex(*param);
|
||||
ASSERT_NE(nullptr, index);
|
||||
index->Open(index_name, {StorageOptions::StorageType::kBufferPool, true});
|
||||
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
|
||||
std::vector<float> vec(kDimension, 2.0f);
|
||||
VectorData vd;
|
||||
vd.vector = DenseVector{vec.data()};
|
||||
ASSERT_EQ(0, index->Add(vd, 2));
|
||||
ASSERT_TRUE(index->IsDirty());
|
||||
|
||||
ASSERT_EQ(0, index->Flush());
|
||||
ASSERT_FALSE(index->IsDirty());
|
||||
|
||||
index->Close();
|
||||
}
|
||||
|
||||
zvec::test_util::RemoveTestFiles(index_name);
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) || defined(__GNUG__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
Loading…
Reference in New Issue