enhance:refactor flush source (#50300)

issue: #50425

Signed-off-by: luzhang <luzhang@zilliz.com>
Co-authored-by: luzhang <luzhang@zilliz.com>
This commit is contained in:
zhagnlu
2026-06-10 10:30:19 +08:00
committed by GitHub
co-authored by luzhang
parent a87bc5844c
commit f756c52a22
105 changed files with 7834 additions and 2727 deletions
+1
View File
@@ -994,6 +994,7 @@ common:
enabled: true # enable split by average size policy in storage v2
threshold: 1024 # split by average size policy threshold(in bytes) in storage v2
useLoonFFI: false
enableGrowingSourceFlush: true # enable flushing growing segment payload from QueryNode growing source through StorageV3 manifest path
# Default value: auto
# Valid values: [auto, avx512, avx2, avx, sse4_2]
# This configuration is only used by querynode and indexnode, it selects CPU instruction set for Searching and Index-building.
+16 -10
View File
@@ -312,17 +312,23 @@ FieldDataImpl<Type, is_type_entire_row>::FillFieldData(
std::dynamic_pointer_cast<arrow::BinaryArray>(array);
AssertInfo(geometry_array != nullptr,
"null geometry arrow binary array");
std::vector<uint8_t> values(element_count);
for (size_t index = 0; index < element_count; ++index) {
values[index] = *geometry_array->GetValue(index, 0);
if constexpr (std::is_same_v<Type, std::string>) {
std::vector<std::string> values(element_count);
for (size_t index = 0; index < element_count; ++index) {
auto sv = geometry_array->GetView(index);
values[index].assign(sv.data(), sv.size());
}
if (nullable_) {
return FillFieldData(values.data(),
array->null_bitmap_data(),
element_count,
array->offset());
}
return FillFieldData(values.data(), element_count);
}
if (nullable_) {
return FillFieldData(values.data(),
array->null_bitmap_data(),
element_count,
array->offset());
}
return FillFieldData(values.data(), element_count);
ThrowInfo(DataTypeInvalid,
"GEOMETRY arrow data must be filled into string-backed "
"FieldData");
}
case DataType::ARRAY: {
auto array_array =
@@ -136,6 +136,7 @@ VectorBase::set_data_raw(ssize_t element_offset,
FIELD_DATA(data, timestamptz).data(),
element_count);
}
case DataType::STRING:
case DataType::VARCHAR:
case DataType::TEXT: {
auto& field_data = FIELD_DATA(data, string);
+1
View File
@@ -1650,6 +1650,7 @@ class InsertRecordGrowing {
field_id, size_per_chunk, scalar_mmap_descriptor);
return;
}
case DataType::STRING:
case DataType::VARCHAR:
case DataType::TEXT: {
this->append_data<std::string>(
+11
View File
@@ -72,6 +72,16 @@ class SegcoreConfig {
return enable_interim_segment_index_;
}
void
set_enable_growing_source_flush(bool enable_growing_source_flush) {
this->enable_growing_source_flush_ = enable_growing_source_flush;
}
bool
get_enable_growing_source_flush() const {
return enable_growing_source_flush_;
}
void
set_sub_dim(int64_t sub_dim) {
sub_dim_ = sub_dim;
@@ -205,6 +215,7 @@ class SegcoreConfig {
knowhere::IndexEnum::INDEX_FAISS_SCANN_DVR,
};
inline static bool enable_interim_segment_index_ = false;
inline static bool enable_growing_source_flush_ = false;
inline static int64_t chunk_rows_ = 32 * 1024;
inline static int64_t nlist_ = 100;
inline static int64_t nprobe_ = 4;
@@ -259,6 +259,17 @@ ExtractArrayLengths(const proto::schema::FieldData& field_data,
}
}
bool
SchemaHasTextField(const Schema& schema) {
for ([[maybe_unused]] const auto& [field_id, field_meta] :
schema.get_fields()) {
if (field_meta.get_data_type() == DataType::TEXT) {
return true;
}
}
return false;
}
} // anonymous namespace
void
@@ -312,6 +323,16 @@ SegmentGrowingImpl::mask_with_delete(BitsetTypeView& bitset,
void
SegmentGrowingImpl::try_remove_chunks(FieldId fieldId) {
if (SchemaHasTextField(*schema_) ||
segcore_config_.get_enable_growing_source_flush()) {
// Growing-source flush needs raw field chunks to persist a growing
// segment through milvus-storage. Interim indexes may also contain raw
// vector data, but FlushGrowingSegmentData reads directly from
// insert_record_ chunks. Keep raw chunks until the flush path can
// reliably export from indexes too.
return;
}
//remove the chunk data to reduce memory consumption
auto& field_meta = schema_->operator[](fieldId);
auto data_type = field_meta.get_data_type();
@@ -627,36 +648,36 @@ SegmentGrowingImpl::Insert(int64_t reserved_offset,
&insert_record_proto->fields_data(data_offset),
field_meta);
}
if (!indexing_record_.HasRawData(field_id)) {
// special handling for TEXT fields with spillover
if (field_meta.get_data_type() == DataType::TEXT) {
auto spillover = GetTextLobSpillover(field_id);
AssertInfo(spillover != nullptr,
"TEXT field must have spillover");
const auto& field_data =
insert_record_proto->fields_data(data_offset);
const auto& string_data = field_data.scalars().string_data();
// Growing-source flush reads raw data from insert_record_. Keep it
// populated even when the interim index can also serve raw vector data.
// Otherwise later inserts after index sync would be visible by row count
// but missing from field chunks during FlushGrowingSegmentData.
if (field_meta.get_data_type() == DataType::TEXT) {
auto spillover = GetTextLobSpillover(field_id);
AssertInfo(spillover != nullptr, "TEXT field must have spillover");
const auto& field_data =
insert_record_proto->fields_data(data_offset);
const auto& string_data = field_data.scalars().string_data();
std::vector<std::string> ref_strings(num_rows);
for (int64_t i = 0; i < num_rows; i++) {
const auto& text = string_data.data(i);
ref_strings[i] = spillover->WriteAndEncode(text);
}
auto* vec_base = insert_record_.get_data_base(field_id);
auto* string_vec =
dynamic_cast<ConcurrentVector<std::string>*>(vec_base);
AssertInfo(string_vec != nullptr,
"TEXT field must use ConcurrentVector<std::string>");
string_vec->set_data_raw(
reserved_offset, ref_strings.data(), num_rows);
} else {
insert_record_.get_data_base(field_id)->set_data_raw(
reserved_offset,
num_rows,
&insert_record_proto->fields_data(data_offset),
field_meta);
std::vector<std::string> ref_strings(num_rows);
for (int64_t i = 0; i < num_rows; i++) {
const auto& text = string_data.data(i);
ref_strings[i] = spillover->WriteAndEncode(text);
}
auto* vec_base = insert_record_.get_data_base(field_id);
auto* string_vec =
dynamic_cast<ConcurrentVector<std::string>*>(vec_base);
AssertInfo(string_vec != nullptr,
"TEXT field must use ConcurrentVector<std::string>");
string_vec->set_data_raw(
reserved_offset, ref_strings.data(), num_rows);
} else {
insert_record_.get_data_base(field_id)->set_data_raw(
reserved_offset,
num_rows,
&insert_record_proto->fields_data(data_offset),
field_meta);
}
//insert vector data into index
@@ -762,8 +783,12 @@ SegmentGrowingImpl::LoadFieldData(const LoadFieldDataInfo& infos,
milvus::OpContext* op_ctx) {
// Note: op_ctx is currently unused in growing segments but kept for interface consistency
(void)op_ctx;
AssertInfo(
!(infos.storage_version == STORAGE_V2 && SchemaHasTextField(*schema_)),
"TEXT growing segment cannot be loaded from StorageV2 binlogs; "
"StorageV3 manifest is required");
switch (infos.storage_version) {
case 2:
case STORAGE_V2:
load_column_group_data_internal(infos);
break;
default:
@@ -882,13 +907,13 @@ SegmentGrowingImpl::load_field_data_common(
auto field_meta = (*schema_)[field_id];
if (!indexing_record_.HasRawData(field_id)) {
if (insert_record_.is_valid_data_exist(field_id)) {
insert_record_.get_valid_data(field_id)->set_data_raw(field_data);
}
insert_record_.get_data_base(field_id)->set_data_raw(reserved_offset,
field_data);
// Growing-source flush reads raw data from insert_record_. Keep it
// populated even when an interim index can provide raw vector data.
if (insert_record_.is_valid_data_exist(field_id)) {
insert_record_.get_valid_data(field_id)->set_data_raw(field_data);
}
insert_record_.get_data_base(field_id)->set_data_raw(reserved_offset,
field_data);
if (segcore_config_.get_enable_interim_segment_index()) {
auto offset = reserved_offset;
for (auto& data : field_data) {
@@ -2258,6 +2283,9 @@ SegmentGrowingImpl::Load(milvus::tracer::TraceContext& trace_ctx,
field_data_info.load_priority = load_info_.priority();
auto manifest_path = load_info_.manifest_path();
AssertInfo(!(manifest_path.empty() && SchemaHasTextField(*schema_)),
"TEXT growing segment cannot be loaded without a StorageV3 "
"manifest");
if (manifest_path != "") {
LoadColumnsGroups(manifest_path);
return;
+2
View File
@@ -648,6 +648,7 @@ CreateScalarDataArrayFrom(const void* data_raw,
obj->mutable_data()->Add(data, data + count);
break;
}
case DataType::STRING:
case DataType::VARCHAR:
case DataType::TEXT: {
auto data = reinterpret_cast<const std::string*>(data_raw);
@@ -1025,6 +1026,7 @@ MergeDataArray(std::vector<MergeBase>& merge_bases,
*(obj->mutable_data()->Add()) = data[src_offset];
break;
}
case DataType::STRING:
case DataType::VARCHAR:
case DataType::TEXT: {
auto* mutable_src = src_field_data->mutable_scalars()
@@ -451,6 +451,756 @@ TEST_F(FlushGrowingSegmentTest, FlushWithNullableFields) {
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushOrdinaryFieldSemanticsRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
DefaultValueType default_value;
default_value.set_int_data(42);
auto default_fid = schema->AddDebugFieldWithDefaultValue(
"default_i32", DataType::INT32, default_value);
auto nullable_str_fid =
schema->AddDebugField("nullable_str", DataType::VARCHAR, true);
auto json_fid = schema->AddDebugField("json", DataType::JSON, true);
auto array_fid =
schema->AddDebugField("array", DataType::ARRAY, DataType::INT64);
auto geometry_fid =
schema->AddDebugField("geometry", DataType::GEOMETRY, true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 3;
std::vector<int64_t> row_ids = {10, 11, 12};
std::vector<Timestamp> timestamps = {100, 101, 102};
std::vector<int64_t> pks = {1000, 1001, 1002};
std::vector<std::string> nullable_strings = {"alpha", "", "gamma"};
bool nullable_string_valid[N] = {true, false, true};
std::vector<std::string> json_values = {
R"({"k":1,"nested":{"v":"a"}})", "", R"({"k":3,"array":[1,2]})"};
bool json_valid[N] = {true, false, true};
std::vector<ScalarFieldProto> array_values(N);
array_values[0].mutable_long_data()->add_data(1);
array_values[0].mutable_long_data()->add_data(2);
array_values[1].mutable_long_data()->add_data(3);
array_values[2].mutable_long_data()->add_data(4);
array_values[2].mutable_long_data()->add_data(5);
array_values[2].mutable_long_data()->add_data(6);
auto geos_ctx = GEOS_init_r();
std::vector<std::string> geometry_values;
geometry_values.emplace_back(
Geometry(geos_ctx, "POINT (1 2)").to_wkb_string());
geometry_values.emplace_back("");
geometry_values.emplace_back(
Geometry(geos_ctx, "POINT (3 4)").to_wkb_string());
GEOS_finish_r(geos_ctx);
bool geometry_valid[N] = {true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(nullable_strings.data(),
nullable_string_valid,
N,
(*schema)[nullable_str_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
json_values.data(), json_valid, N, (*schema)[json_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
array_values.data(), nullptr, N, (*schema)[array_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
geometry_values.data(), geometry_valid, N, (*schema)[geometry_fid])
.release());
// default_i32 is intentionally absent from insert_data. Growing insert must
// fill it exactly as the DataNode write-buffer path does.
segment->PreInsert(N);
segment->Insert(0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_semantics";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto default_datas = ReadFlushedFieldData(
segment_path, result, default_fid, DataType::INT32, true, 0);
ASSERT_EQ(default_datas.size(), 1);
ASSERT_EQ(default_datas[0]->get_num_rows(), N);
ASSERT_EQ(default_datas[0]->get_valid_rows(), N);
for (int i = 0; i < N; i++) {
ASSERT_TRUE(default_datas[0]->is_valid(i));
EXPECT_EQ(*static_cast<const int32_t*>(default_datas[0]->RawValue(i)),
42);
}
auto string_datas = ReadFlushedFieldData(
segment_path, result, nullable_str_fid, DataType::VARCHAR, true, 0);
ASSERT_EQ(string_datas.size(), 1);
ASSERT_TRUE(string_datas[0]->is_valid(0));
EXPECT_FALSE(string_datas[0]->is_valid(1));
ASSERT_TRUE(string_datas[0]->is_valid(2));
EXPECT_EQ(*static_cast<const std::string*>(string_datas[0]->RawValue(0)),
"alpha");
EXPECT_EQ(*static_cast<const std::string*>(string_datas[0]->RawValue(2)),
"gamma");
auto json_datas = ReadFlushedFieldData(
segment_path, result, json_fid, DataType::JSON, true, 0);
ASSERT_EQ(json_datas.size(), 1);
ASSERT_TRUE(json_datas[0]->is_valid(0));
EXPECT_FALSE(json_datas[0]->is_valid(1));
ASSERT_TRUE(json_datas[0]->is_valid(2));
EXPECT_EQ(std::string(
static_cast<const Json*>(json_datas[0]->RawValue(0))->data()),
json_values[0]);
EXPECT_EQ(std::string(
static_cast<const Json*>(json_datas[0]->RawValue(2))->data()),
json_values[2]);
auto array_datas = ReadFlushedFieldData(segment_path,
result,
array_fid,
DataType::ARRAY,
false,
0,
DataType::INT64);
ASSERT_EQ(array_datas.size(), 1);
auto row0_array =
static_cast<const Array*>(array_datas[0]->RawValue(0))->output_data();
auto row2_array =
static_cast<const Array*>(array_datas[0]->RawValue(2))->output_data();
EXPECT_EQ(row0_array.long_data().data().size(), 2);
EXPECT_EQ(row0_array.long_data().data(0), 1);
EXPECT_EQ(row0_array.long_data().data(1), 2);
EXPECT_EQ(row2_array.long_data().data().size(), 3);
EXPECT_EQ(row2_array.long_data().data(0), 4);
EXPECT_EQ(row2_array.long_data().data(2), 6);
auto geometry_datas = ReadFlushedFieldData(
segment_path, result, geometry_fid, DataType::GEOMETRY, true, 0);
ASSERT_EQ(geometry_datas.size(), 1);
ASSERT_TRUE(geometry_datas[0]->is_valid(0));
EXPECT_FALSE(geometry_datas[0]->is_valid(1));
ASSERT_TRUE(geometry_datas[0]->is_valid(2));
EXPECT_EQ(*static_cast<const std::string*>(geometry_datas[0]->RawValue(0)),
geometry_values[0]);
EXPECT_EQ(*static_cast<const std::string*>(geometry_datas[0]->RawValue(2)),
geometry_values[2]);
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushTimestamptzPartialRangeRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto timestamptz_fid =
schema->AddDebugField("event_time", DataType::TIMESTAMPTZ, true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 5;
std::vector<int64_t> row_ids = {0, 1, 2, 3, 4};
std::vector<Timestamp> timestamps = {100, 101, 102, 103, 104};
std::vector<int64_t> pks = {10, 11, 12, 13, 14};
std::vector<int64_t> event_times = {1700000000000,
1700000001000,
1700000002000,
1700000003000,
1700000004000};
bool event_time_valid[N] = {true, false, true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
event_times.data(), event_time_valid, N, (*schema)[timestamptz_fid])
.release());
segment->PreInsert(N);
segment->Insert(0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_timestamptz";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
constexpr int64_t start = 1;
constexpr int64_t end = 4;
auto status =
FlushGrowingSegmentData(segment.get(), start, end, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, end - start);
auto field_datas = ReadFlushedFieldData(
segment_path, result, timestamptz_fid, DataType::TIMESTAMPTZ, true, 0);
ASSERT_EQ(field_datas.size(), 1);
ASSERT_EQ(field_datas[0]->get_num_rows(), end - start);
EXPECT_FALSE(field_datas[0]->is_valid(0));
ASSERT_TRUE(field_datas[0]->is_valid(1));
EXPECT_EQ(*static_cast<const int64_t*>(field_datas[0]->RawValue(1)),
event_times[2]);
EXPECT_FALSE(field_datas[0]->is_valid(2));
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushNullableScalarTypesPartialRangeRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto bool_fid = schema->AddDebugField("bool_field", DataType::BOOL, true);
auto int8_fid = schema->AddDebugField("int8_field", DataType::INT8, true);
auto int16_fid =
schema->AddDebugField("int16_field", DataType::INT16, true);
auto int64_fid =
schema->AddDebugField("int64_field", DataType::INT64, true);
auto float_fid =
schema->AddDebugField("float_field", DataType::FLOAT, true);
auto double_fid =
schema->AddDebugField("double_field", DataType::DOUBLE, true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 5;
std::vector<int64_t> row_ids = {0, 1, 2, 3, 4};
std::vector<Timestamp> timestamps = {100, 101, 102, 103, 104};
std::vector<int64_t> pks = {10, 11, 12, 13, 14};
bool bool_values[N] = {true, false, true, false, true};
std::vector<int8_t> int8_values = {-2, -1, 0, 1, 2};
std::vector<int16_t> int16_values = {-200, -100, 0, 100, 200};
std::vector<int64_t> int64_values = {1000, 1001, 1002, 1003, 1004};
std::vector<float> float_values = {1.25F, 2.5F, 3.75F, 4.5F, 5.25F};
std::vector<double> double_values = {10.25, 20.5, 30.75, 40.5, 50.25};
bool valid_data[N] = {true, false, true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(bool_values, valid_data, N, (*schema)[bool_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
int8_values.data(), valid_data, N, (*schema)[int8_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
int16_values.data(), valid_data, N, (*schema)[int16_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
int64_values.data(), valid_data, N, (*schema)[int64_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
float_values.data(), valid_data, N, (*schema)[float_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
double_values.data(), valid_data, N, (*schema)[double_fid])
.release());
segment->PreInsert(N);
segment->Insert(0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_nullable_scalars";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
constexpr int64_t start = 1;
constexpr int64_t end = 5;
auto status =
FlushGrowingSegmentData(segment.get(), start, end, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, end - start);
auto bool_datas = ReadFlushedFieldData(
segment_path, result, bool_fid, DataType::BOOL, true, 0);
ASSERT_EQ(bool_datas.size(), 1);
EXPECT_FALSE(bool_datas[0]->is_valid(0));
ASSERT_TRUE(bool_datas[0]->is_valid(1));
EXPECT_EQ(*static_cast<const bool*>(bool_datas[0]->RawValue(1)),
bool_values[2]);
EXPECT_FALSE(bool_datas[0]->is_valid(2));
ASSERT_TRUE(bool_datas[0]->is_valid(3));
EXPECT_EQ(*static_cast<const bool*>(bool_datas[0]->RawValue(3)),
bool_values[4]);
auto int8_datas = ReadFlushedFieldData(
segment_path, result, int8_fid, DataType::INT8, true, 0);
ASSERT_EQ(int8_datas.size(), 1);
ASSERT_TRUE(int8_datas[0]->is_valid(1));
EXPECT_EQ(*static_cast<const int8_t*>(int8_datas[0]->RawValue(1)),
int8_values[2]);
ASSERT_TRUE(int8_datas[0]->is_valid(3));
EXPECT_EQ(*static_cast<const int8_t*>(int8_datas[0]->RawValue(3)),
int8_values[4]);
auto int16_datas = ReadFlushedFieldData(
segment_path, result, int16_fid, DataType::INT16, true, 0);
ASSERT_EQ(int16_datas.size(), 1);
ASSERT_TRUE(int16_datas[0]->is_valid(1));
EXPECT_EQ(*static_cast<const int16_t*>(int16_datas[0]->RawValue(1)),
int16_values[2]);
ASSERT_TRUE(int16_datas[0]->is_valid(3));
EXPECT_EQ(*static_cast<const int16_t*>(int16_datas[0]->RawValue(3)),
int16_values[4]);
auto int64_datas = ReadFlushedFieldData(
segment_path, result, int64_fid, DataType::INT64, true, 0);
ASSERT_EQ(int64_datas.size(), 1);
ASSERT_TRUE(int64_datas[0]->is_valid(1));
EXPECT_EQ(*static_cast<const int64_t*>(int64_datas[0]->RawValue(1)),
int64_values[2]);
ASSERT_TRUE(int64_datas[0]->is_valid(3));
EXPECT_EQ(*static_cast<const int64_t*>(int64_datas[0]->RawValue(3)),
int64_values[4]);
auto float_datas = ReadFlushedFieldData(
segment_path, result, float_fid, DataType::FLOAT, true, 0);
ASSERT_EQ(float_datas.size(), 1);
ASSERT_TRUE(float_datas[0]->is_valid(1));
EXPECT_FLOAT_EQ(*static_cast<const float*>(float_datas[0]->RawValue(1)),
float_values[2]);
ASSERT_TRUE(float_datas[0]->is_valid(3));
EXPECT_FLOAT_EQ(*static_cast<const float*>(float_datas[0]->RawValue(3)),
float_values[4]);
auto double_datas = ReadFlushedFieldData(
segment_path, result, double_fid, DataType::DOUBLE, true, 0);
ASSERT_EQ(double_datas.size(), 1);
ASSERT_TRUE(double_datas[0]->is_valid(1));
EXPECT_DOUBLE_EQ(*static_cast<const double*>(double_datas[0]->RawValue(1)),
double_values[2]);
ASSERT_TRUE(double_datas[0]->is_valid(3));
EXPECT_DOUBLE_EQ(*static_cast<const double*>(double_datas[0]->RawValue(3)),
double_values[4]);
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushVectorArrayRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto vec_array_fid = schema->AddDebugVectorArrayField(
"emb_list", DataType::VECTOR_FLOAT, 4, knowhere::metric::L2);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 4;
constexpr int array_len = 3;
auto dataset = DataGen(schema, N, 42, 0, 1, array_len);
segment->PreInsert(N);
segment->Insert(0,
N,
dataset.row_ids_.data(),
dataset.timestamps_.data(),
dataset.raw_);
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_vector_array";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto field_datas = ReadFlushedFieldData(segment_path,
result,
vec_array_fid,
DataType::VECTOR_ARRAY,
false,
4,
DataType::VECTOR_FLOAT);
ASSERT_EQ(field_datas.size(), 1);
ASSERT_EQ(field_datas[0]->get_num_rows(), N);
auto expected = dataset.get_col<VectorFieldProto>(vec_array_fid);
for (int i = 0; i < N; i++) {
auto actual = static_cast<const milvus::VectorArray*>(
field_datas[0]->RawValue(i));
ASSERT_NE(actual, nullptr);
EXPECT_EQ(actual->length(), array_len);
EXPECT_EQ(actual->output_data().SerializeAsString(),
expected[i].SerializeAsString());
}
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushVectorArrayElementTypesRoundTrip) {
struct Case {
DataType element_type;
int64_t dim;
std::string metric_type;
std::string segment_suffix;
};
std::vector<Case> cases = {
{DataType::VECTOR_BINARY, 16, "JACCARD", "binary"},
{DataType::VECTOR_FLOAT16, 4, "L2", "fp16"},
{DataType::VECTOR_BFLOAT16, 4, "L2", "bf16"},
{DataType::VECTOR_INT8, 4, "L2", "int8"},
};
for (const auto& test_case : cases) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto vec_array_fid =
schema->AddDebugVectorArrayField("emb_list",
test_case.element_type,
test_case.dim,
test_case.metric_type);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 4;
constexpr int array_len = 3;
auto dataset = DataGen(schema, N, 42, 0, 1, array_len);
segment->PreInsert(N);
segment->Insert(0,
N,
dataset.row_ids_.data(),
dataset.timestamps_.data(),
dataset.raw_);
CFlushConfig config{};
std::string segment_path =
test_dir_ + "/segment_vector_array_" + test_case.segment_suffix;
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto field_datas = ReadFlushedFieldData(segment_path,
result,
vec_array_fid,
DataType::VECTOR_ARRAY,
false,
test_case.dim,
test_case.element_type);
ASSERT_EQ(field_datas.size(), 1);
ASSERT_EQ(field_datas[0]->get_num_rows(), N);
auto expected = dataset.get_col<VectorFieldProto>(vec_array_fid);
for (int i = 0; i < N; i++) {
auto actual = static_cast<const milvus::VectorArray*>(
field_datas[0]->RawValue(i));
ASSERT_NE(actual, nullptr);
EXPECT_EQ(actual->length(), array_len);
EXPECT_EQ(actual->get_element_type(), test_case.element_type);
EXPECT_EQ(actual->output_data().SerializeAsString(),
expected[i].SerializeAsString());
}
FreeFlushResult(&result);
}
}
TEST_F(FlushGrowingSegmentTest, FlushStringAndTextRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto string_fid = schema->AddDebugField("str", DataType::STRING, true);
auto text_fid = schema->AddDebugField("text", DataType::TEXT, true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 3;
std::vector<int64_t> row_ids = {0, 1, 2};
std::vector<Timestamp> timestamps = {10, 11, 12};
std::vector<int64_t> pks = {100, 101, 102};
std::vector<std::string> strings = {"plain-string", "", "tail-string"};
bool string_valid[N] = {true, false, true};
std::vector<std::string> texts = {
"short text", "", std::string(512, 'x') + "-tail"};
bool text_valid[N] = {true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
strings.data(), string_valid, N, (*schema)[string_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(texts.data(), text_valid, N, (*schema)[text_fid])
.release());
segment->PreInsert(N);
segment->Insert(0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_string_text";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto string_datas = ReadFlushedFieldData(
segment_path, result, string_fid, DataType::STRING, true, 0);
ASSERT_EQ(string_datas.size(), 1);
ASSERT_TRUE(string_datas[0]->is_valid(0));
EXPECT_FALSE(string_datas[0]->is_valid(1));
ASSERT_TRUE(string_datas[0]->is_valid(2));
EXPECT_EQ(*static_cast<const std::string*>(string_datas[0]->RawValue(0)),
strings[0]);
EXPECT_EQ(*static_cast<const std::string*>(string_datas[0]->RawValue(2)),
strings[2]);
auto text_datas = ReadFlushedFieldData(
segment_path, result, text_fid, DataType::TEXT, true, 0);
ASSERT_EQ(text_datas.size(), 1);
ASSERT_TRUE(text_datas[0]->is_valid(0));
EXPECT_FALSE(text_datas[0]->is_valid(1));
ASSERT_TRUE(text_datas[0]->is_valid(2));
EXPECT_EQ(*static_cast<const std::string*>(text_datas[0]->RawValue(0)),
texts[0]);
EXPECT_EQ(*static_cast<const std::string*>(text_datas[0]->RawValue(2)),
texts[2]);
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushArrayElementTypesRoundTrip) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto bool_array_fid =
schema->AddDebugField("bool_array", DataType::ARRAY, DataType::BOOL);
auto int_array_fid =
schema->AddDebugField("int_array", DataType::ARRAY, DataType::INT32);
auto string_array_fid = schema->AddDebugField(
"string_array", DataType::ARRAY, DataType::VARCHAR);
auto double_array_fid = schema->AddDebugField(
"double_array", DataType::ARRAY, DataType::DOUBLE, true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 3;
std::vector<int64_t> row_ids = {0, 1, 2};
std::vector<Timestamp> timestamps = {10, 11, 12};
std::vector<int64_t> pks = {100, 101, 102};
std::vector<ScalarFieldProto> bool_arrays(N);
bool_arrays[0].mutable_bool_data()->add_data(true);
bool_arrays[0].mutable_bool_data()->add_data(false);
bool_arrays[1].mutable_bool_data()->add_data(false);
bool_arrays[2].mutable_bool_data()->add_data(true);
bool_arrays[2].mutable_bool_data()->add_data(true);
std::vector<ScalarFieldProto> int_arrays(N);
int_arrays[0].mutable_int_data()->add_data(1);
int_arrays[0].mutable_int_data()->add_data(2);
int_arrays[1].mutable_int_data()->add_data(3);
int_arrays[2].mutable_int_data()->add_data(4);
int_arrays[2].mutable_int_data()->add_data(5);
std::vector<ScalarFieldProto> string_arrays(N);
string_arrays[0].mutable_string_data()->add_data("a");
string_arrays[0].mutable_string_data()->add_data("b");
string_arrays[1].mutable_string_data()->add_data("c");
string_arrays[2].mutable_string_data()->add_data("d");
string_arrays[2].mutable_string_data()->add_data("e");
std::vector<ScalarFieldProto> double_arrays(N);
double_arrays[0].mutable_double_data()->add_data(1.25);
double_arrays[0].mutable_double_data()->add_data(2.5);
double_arrays[1].mutable_double_data()->add_data(3.75);
double_arrays[2].mutable_double_data()->add_data(4.5);
bool double_valid[N] = {true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
bool_arrays.data(), nullptr, N, (*schema)[bool_array_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
int_arrays.data(), nullptr, N, (*schema)[int_array_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
string_arrays.data(), nullptr, N, (*schema)[string_array_fid])
.release());
insert_data->mutable_fields_data()->AddAllocated(
CreateDataArrayFrom(
double_arrays.data(), double_valid, N, (*schema)[double_array_fid])
.release());
segment->PreInsert(N);
segment->Insert(0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path = test_dir_ + "/segment_arrays";
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto bool_datas = ReadFlushedFieldData(segment_path,
result,
bool_array_fid,
DataType::ARRAY,
false,
0,
DataType::BOOL);
ASSERT_EQ(bool_datas.size(), 1);
EXPECT_EQ(static_cast<const Array*>(bool_datas[0]->RawValue(0))
->output_data()
.SerializeAsString(),
bool_arrays[0].SerializeAsString());
EXPECT_EQ(static_cast<const Array*>(bool_datas[0]->RawValue(2))
->output_data()
.SerializeAsString(),
bool_arrays[2].SerializeAsString());
auto int_datas = ReadFlushedFieldData(segment_path,
result,
int_array_fid,
DataType::ARRAY,
false,
0,
DataType::INT32);
ASSERT_EQ(int_datas.size(), 1);
EXPECT_EQ(static_cast<const Array*>(int_datas[0]->RawValue(0))
->output_data()
.SerializeAsString(),
int_arrays[0].SerializeAsString());
EXPECT_EQ(static_cast<const Array*>(int_datas[0]->RawValue(2))
->output_data()
.SerializeAsString(),
int_arrays[2].SerializeAsString());
auto string_datas = ReadFlushedFieldData(segment_path,
result,
string_array_fid,
DataType::ARRAY,
false,
0,
DataType::VARCHAR);
ASSERT_EQ(string_datas.size(), 1);
EXPECT_EQ(static_cast<const Array*>(string_datas[0]->RawValue(0))
->output_data()
.SerializeAsString(),
string_arrays[0].SerializeAsString());
EXPECT_EQ(static_cast<const Array*>(string_datas[0]->RawValue(2))
->output_data()
.SerializeAsString(),
string_arrays[2].SerializeAsString());
auto double_datas = ReadFlushedFieldData(segment_path,
result,
double_array_fid,
DataType::ARRAY,
true,
0,
DataType::DOUBLE);
ASSERT_EQ(double_datas.size(), 1);
ASSERT_TRUE(double_datas[0]->is_valid(0));
EXPECT_FALSE(double_datas[0]->is_valid(1));
ASSERT_TRUE(double_datas[0]->is_valid(2));
EXPECT_EQ(static_cast<const Array*>(double_datas[0]->RawValue(0))
->output_data()
.SerializeAsString(),
double_arrays[0].SerializeAsString());
EXPECT_EQ(static_cast<const Array*>(double_datas[0]->RawValue(2))
->output_data()
.SerializeAsString(),
double_arrays[2].SerializeAsString());
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushNullableFloatVectorKeepsCompactMapping) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
@@ -586,6 +1336,150 @@ TEST_F(FlushGrowingSegmentTest, FlushNullableInt8VectorKeepsCompactMapping) {
FreeFlushResult(&result);
}
TEST_F(FlushGrowingSegmentTest, FlushNullableFixedWidthVectorTypesRoundTrip) {
struct Case {
DataType data_type;
int64_t dim;
std::string metric_type;
std::string segment_suffix;
};
std::vector<Case> cases = {
{DataType::VECTOR_BINARY, 16, "JACCARD", "binary"},
{DataType::VECTOR_FLOAT16, 2, "L2", "fp16"},
{DataType::VECTOR_BFLOAT16, 2, "L2", "bf16"},
};
for (const auto& test_case : cases) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
auto vec_fid = schema->AddDebugField("vec",
test_case.data_type,
test_case.dim,
test_case.metric_type,
true);
schema->set_primary_field_id(pk_fid);
auto segment = CreateGrowingSegment(schema, empty_index_meta);
ASSERT_NE(segment, nullptr);
constexpr int N = 3;
std::vector<int64_t> row_ids = {0, 1, 2};
std::vector<Timestamp> timestamps = {10, 11, 12};
std::vector<int64_t> pks = {100, 101, 102};
bool valid_data[N] = {true, false, true};
auto insert_data = std::make_unique<InsertRecordProto>();
insert_data->set_num_rows(N);
auto pk_array =
CreateDataArrayFrom(pks.data(), nullptr, N, (*schema)[pk_fid]);
insert_data->mutable_fields_data()->AddAllocated(pk_array.release());
std::vector<uint8_t> binary_vectors = {0x01, 0x02, 0xA0, 0xB0};
std::vector<float16> fp16_vectors = {1, 2, 3, 4};
std::vector<bfloat16> bf16_vectors = {5, 6, 7, 8};
const void* vector_data = nullptr;
switch (test_case.data_type) {
case DataType::VECTOR_BINARY:
vector_data = binary_vectors.data();
break;
case DataType::VECTOR_FLOAT16:
vector_data = fp16_vectors.data();
break;
case DataType::VECTOR_BFLOAT16:
vector_data = bf16_vectors.data();
break;
default:
FAIL() << "unexpected vector type";
}
auto vec_array = CreateVectorDataArrayFrom(
vector_data, valid_data, N, 2, (*schema)[vec_fid]);
insert_data->mutable_fields_data()->AddAllocated(vec_array.release());
segment->PreInsert(N);
segment->Insert(
0, N, row_ids.data(), timestamps.data(), insert_data.get());
CFlushConfig config{};
std::string segment_path =
test_dir_ + "/segment_nullable_" + test_case.segment_suffix;
config.segment_path = segment_path.c_str();
config.read_version = -1;
config.retry_limit = 3;
config.text_field_ids = nullptr;
config.text_lob_paths = nullptr;
config.num_text_columns = 0;
CFlushResult result;
auto status =
FlushGrowingSegmentData(segment.get(), 0, N, &config, &result);
ASSERT_EQ(status.error_code, Success) << status.error_msg;
ASSERT_EQ(result.num_rows, N);
auto field_datas = ReadFlushedFieldData(segment_path,
result,
vec_fid,
test_case.data_type,
true,
test_case.dim);
ASSERT_EQ(field_datas.size(), 1);
auto field_data = field_datas[0];
ASSERT_EQ(field_data->get_num_rows(), N);
ASSERT_EQ(field_data->get_valid_rows(), 2);
ASSERT_TRUE(field_data->is_valid(0));
EXPECT_FALSE(field_data->is_valid(1));
ASSERT_TRUE(field_data->is_valid(2));
switch (test_case.data_type) {
case DataType::VECTOR_BINARY: {
auto row0 =
static_cast<const uint8_t*>(field_data->RawValue(0));
auto row2 =
static_cast<const uint8_t*>(field_data->RawValue(2));
ASSERT_NE(row0, nullptr);
ASSERT_NE(row2, nullptr);
EXPECT_EQ(std::vector<uint8_t>(row0, row0 + 2),
std::vector<uint8_t>({0x01, 0x02}));
EXPECT_EQ(std::vector<uint8_t>(row2, row2 + 2),
std::vector<uint8_t>({0xA0, 0xB0}));
break;
}
case DataType::VECTOR_FLOAT16: {
auto row0 =
static_cast<const float16*>(field_data->RawValue(0));
auto row2 =
static_cast<const float16*>(field_data->RawValue(2));
ASSERT_NE(row0, nullptr);
ASSERT_NE(row2, nullptr);
EXPECT_EQ(std::vector<float16>(row0, row0 + 2),
std::vector<float16>({1, 2}));
EXPECT_EQ(std::vector<float16>(row2, row2 + 2),
std::vector<float16>({3, 4}));
break;
}
case DataType::VECTOR_BFLOAT16: {
auto row0 =
static_cast<const bfloat16*>(field_data->RawValue(0));
auto row2 =
static_cast<const bfloat16*>(field_data->RawValue(2));
ASSERT_NE(row0, nullptr);
ASSERT_NE(row2, nullptr);
EXPECT_EQ(std::vector<bfloat16>(row0, row0 + 2),
std::vector<bfloat16>({5, 6}));
EXPECT_EQ(std::vector<bfloat16>(row2, row2 + 2),
std::vector<bfloat16>({7, 8}));
break;
}
default:
FAIL() << "unexpected vector type";
}
FreeFlushResult(&result);
}
}
TEST_F(FlushGrowingSegmentTest, FlushNullableSparseVectorKeepsCompactMapping) {
auto schema = std::make_shared<Schema>();
auto pk_fid = schema->AddDebugField("pk", DataType::INT64);
@@ -50,6 +50,13 @@ SegcoreSetEnableInterminSegmentIndex(const bool value) {
config.set_enable_interim_segment_index(value);
}
extern "C" void
SegcoreSetEnableGrowingSourceFlush(const bool value) {
milvus::segcore::SegcoreConfig& config =
milvus::segcore::SegcoreConfig::default_config();
config.set_enable_growing_source_flush(value);
}
extern "C" void
SegcoreSetEnableGeometryCache(const bool value) {
milvus::segcore::SegcoreConfig& config =
@@ -29,6 +29,9 @@ SegcoreSetChunkRows(const int64_t);
void
SegcoreSetEnableInterminSegmentIndex(const bool);
void
SegcoreSetEnableGrowingSourceFlush(const bool);
void
SegcoreSetEnableGeometryCache(const bool);
+84 -1
View File
@@ -937,6 +937,7 @@ struct FieldInfo {
milvus::FieldId field_id;
std::string field_name;
milvus::DataType data_type;
milvus::DataType element_type;
bool nullable;
int64_t dim; // for vector types
const milvus::segcore::VectorBase* vec_base;
@@ -958,6 +959,7 @@ GetElementByteWidth(milvus::DataType data_type, int64_t dim) {
case milvus::DataType::FLOAT:
return 4;
case milvus::DataType::INT64:
case milvus::DataType::TIMESTAMPTZ:
case milvus::DataType::DOUBLE:
return 8;
case milvus::DataType::VECTOR_FLOAT:
@@ -1275,6 +1277,47 @@ BuildTextArrayForChunkWithSpillover(
return builder.Finish();
}
arrow::Result<std::shared_ptr<arrow::Array>>
BuildVectorArrayForChunk(const FieldInfo& field_info,
int64_t start_offset,
int64_t num_rows) {
if (field_info.valid_data) {
return arrow::Status::Invalid(
"VECTOR_ARRAY does not support null rows");
}
auto vector_array_vec = dynamic_cast<
const milvus::segcore::ConcurrentVector<milvus::VectorArray>*>(
field_info.vec_base);
if (!vector_array_vec) {
return arrow::Status::Invalid("Expected ConcurrentVector<VectorArray>");
}
auto byte_width = milvus::vector_bytes_per_element(field_info.element_type,
field_info.dim);
auto value_builder = std::make_shared<arrow::FixedSizeBinaryBuilder>(
arrow::fixed_size_binary(byte_width));
arrow::ListBuilder builder(arrow::default_memory_pool(), value_builder);
ARROW_RETURN_NOT_OK(builder.Reserve(num_rows));
for (int64_t i = 0; i < num_rows; i++) {
const auto& vector_array = (*vector_array_vec)[start_offset + i];
if (vector_array.get_element_type() != field_info.element_type) {
return arrow::Status::Invalid("VECTOR_ARRAY element type mismatch");
}
if (vector_array.dim() != field_info.dim) {
return arrow::Status::Invalid("VECTOR_ARRAY dim mismatch");
}
ARROW_RETURN_NOT_OK(builder.Append());
ARROW_RETURN_NOT_OK(value_builder->AppendValues(
reinterpret_cast<const uint8_t*>(vector_array.data()),
vector_array.length()));
}
return builder.Finish();
}
// build boolean array for a chunk - booleans need special handling
arrow::Result<std::shared_ptr<arrow::Array>>
BuildBoolArrayForChunk(
@@ -1343,6 +1386,7 @@ BuildArrayForChunk(const FieldInfo& field_info,
global_offset);
case milvus::DataType::INT64:
case milvus::DataType::TIMESTAMPTZ:
return WrapChunkAsArrowArray<arrow::Int64Array>(
get_data_ptr(),
num_rows,
@@ -1450,6 +1494,29 @@ BuildArrayForChunk(const FieldInfo& field_info,
return builder.Finish();
}
case milvus::DataType::GEOMETRY: {
auto geometry_vec = dynamic_cast<
const milvus::segcore::ConcurrentVector<std::string>*>(
field_info.vec_base);
if (!geometry_vec) {
return arrow::Status::Invalid(
"Expected ConcurrentVector<std::string> for GEOMETRY");
}
arrow::BinaryBuilder builder;
ARROW_RETURN_NOT_OK(builder.Reserve(num_rows));
for (int64_t i = 0; i < num_rows; i++) {
int64_t offset = global_offset + i;
if (field_info.valid_data &&
!field_info.valid_data->is_valid(offset)) {
ARROW_RETURN_NOT_OK(builder.AppendNull());
} else {
auto wkb = geometry_vec->view_element(offset);
ARROW_RETURN_NOT_OK(builder.Append(wkb.data(), wkb.size()));
}
}
return builder.Finish();
}
case milvus::DataType::VECTOR_FLOAT:
case milvus::DataType::VECTOR_BINARY:
case milvus::DataType::VECTOR_FLOAT16:
@@ -1473,6 +1540,10 @@ BuildArrayForChunk(const FieldInfo& field_info,
return BuildSparseFloatVectorArrayForChunk(
field_info, global_offset, num_rows);
case milvus::DataType::VECTOR_ARRAY:
return BuildVectorArrayForChunk(
field_info, global_offset, num_rows);
default:
return arrow::Status::NotImplemented("Unsupported data type");
}
@@ -1537,6 +1608,7 @@ FlushGrowingSegmentData(CSegmentInterface c_segment,
info.field_id = RowFieldID;
info.field_name = field_meta.get_name().get();
info.data_type = field_meta.get_data_type();
info.element_type = milvus::DataType::NONE;
info.nullable = field_meta.is_nullable();
info.dim = 0;
info.vec_base = &insert_record.row_ids_;
@@ -1586,7 +1658,10 @@ FlushGrowingSegmentData(CSegmentInterface c_segment,
!milvus::IsSparseFloatVectorDataType(data_type)
? field_meta.get_dim()
: 0;
auto arrow_type = milvus::GetArrowDataType(data_type, dim);
auto arrow_type = data_type == milvus::DataType::VECTOR_ARRAY
? milvus::GetArrowDataTypeForVectorArray(
field_meta.get_element_type(), dim)
: milvus::GetArrowDataType(data_type, dim);
if (field_meta.is_nullable() &&
IsSupportedNullableVectorDataType(data_type)) {
arrow_type = arrow::binary();
@@ -1596,6 +1671,7 @@ FlushGrowingSegmentData(CSegmentInterface c_segment,
info.field_id = field_id;
info.field_name = field_meta.get_name().get();
info.data_type = data_type;
info.element_type = field_meta.get_element_type();
info.nullable = field_meta.is_nullable();
info.dim = dim;
info.vec_base = vec_base;
@@ -1624,6 +1700,13 @@ FlushGrowingSegmentData(CSegmentInterface c_segment,
metadata_keys.push_back(DIM_KEY);
metadata_values.push_back(std::to_string(dim));
}
if (data_type == milvus::DataType::VECTOR_ARRAY) {
metadata_keys.push_back(ELEMENT_TYPE_KEY_FOR_ARROW);
metadata_values.push_back(std::to_string(
static_cast<int>(field_meta.get_element_type())));
metadata_keys.push_back(DIM_KEY);
metadata_values.push_back(std::to_string(dim));
}
auto metadata =
arrow::KeyValueMetadata::Make(metadata_keys, metadata_values);
arrow_fields.push_back(arrow::field(std::to_string(field_id.get()),
+32 -3
View File
@@ -647,6 +647,10 @@ func (s *Server) SaveBinlogPaths(ctx context.Context, req *datapb.SaveBinlogPath
log.Warn("failed to get segment, the segment not healthy", zap.Error(err))
return merr.Status(err), nil
}
if err := s.validateTextSegmentStorage(req); err != nil {
log.Warn("invalid TEXT segment storage format", zap.Error(err))
return merr.Status(err), nil
}
// Set storage version
operators = append(operators, SetStorageVersion(req.GetSegmentID(), req.GetStorageVersion()))
@@ -739,6 +743,27 @@ func (s *Server) SaveBinlogPaths(ctx context.Context, req *datapb.SaveBinlogPath
return merr.Success(), nil
}
func (s *Server) validateTextSegmentStorage(req *datapb.SaveBinlogPathsRequest) error {
if req.GetSegLevel() == datapb.SegmentLevel_L0 || req.GetDropped() {
return nil
}
if !s.meta.collectionHasTextFields(req.GetCollectionID()) {
return nil
}
if req.GetStorageVersion() < storage.StorageV3 {
return merr.WrapErrParameterInvalidMsg(
"TEXT segment %d must be saved with StorageV3 manifest, got storage version %d",
req.GetSegmentID(),
req.GetStorageVersion())
}
if req.GetManifestPath() == "" {
return merr.WrapErrParameterInvalidMsg(
"TEXT segment %d requires non-empty StorageV3 manifest path",
req.GetSegmentID())
}
return nil
}
// DropVirtualChannel notifies vchannel dropped
// And contains the remaining data log & checkpoint to update
func (s *Server) DropVirtualChannel(ctx context.Context, req *datapb.DropVirtualChannelRequest) (*datapb.DropVirtualChannelResponse, error) {
@@ -1499,12 +1524,16 @@ func (s *Server) GetFlushState(ctx context.Context, req *datapb.GetFlushStateReq
for _, channel := range channels {
cp := s.meta.GetChannelCheckpoint(channel.GetName())
if cp == nil || cp.GetTimestamp() < req.GetFlushTs() {
cpTs := uint64(0)
if cp != nil {
cpTs = cp.GetTimestamp()
}
if cp == nil || cpTs < req.GetFlushTs() {
resp.Flushed = false
log.RatedInfo(10, "GetFlushState failed, channel unflushed", zap.String("channel", channel.GetName()),
zap.Time("CP", tsoutil.PhysicalTime(cp.GetTimestamp())),
zap.Duration("lag", tsoutil.PhysicalTime(req.GetFlushTs()).Sub(tsoutil.PhysicalTime(cp.GetTimestamp()))))
zap.Time("CP", tsoutil.PhysicalTime(cpTs)),
zap.Duration("lag", tsoutil.PhysicalTime(req.GetFlushTs()).Sub(tsoutil.PhysicalTime(cpTs))))
return resp, nil
}
}
+101 -1
View File
@@ -79,7 +79,7 @@ func (s *ServerSuite) SetupSuite() {
<-ctx.Done()
return ctx.Err()
})
b.EXPECT().GetLatestWALLocated(mock.Anything, mock.Anything).Return(0, true)
b.EXPECT().GetLatestWALLocated(mock.Anything, mock.Anything).Return(0, true).Maybe()
balance.Register(b)
}
@@ -149,6 +149,21 @@ func (s *ServerSuite) TestGetFlushState_ByFlushTs() {
}, resp)
}
func (s *ServerSuite) TestGetFlushState_ByFlushTsMissingCheckpoint() {
s.mockMixCoord.EXPECT().DescribeCollectionInternal(mock.Anything, mock.Anything).Return(&milvuspb.DescribeCollectionResponse{
Status: merr.Success(),
CollectionID: 0,
VirtualChannelNames: []string{"missing-cp-channel"},
}, nil)
resp, err := s.testServer.GetFlushState(context.TODO(), &datapb.GetFlushStateRequest{FlushTs: 13})
s.NoError(err)
s.EqualValues(&milvuspb.GetFlushStateResponse{
Status: merr.Success(),
Flushed: false,
}, resp)
}
func (s *ServerSuite) TestGetFlushState_BySegment() {
s.mockMixCoord.EXPECT().DescribeCollectionInternal(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, req *milvuspb.DescribeCollectionRequest) (*milvuspb.DescribeCollectionResponse, error) {
return &milvuspb.DescribeCollectionResponse{
@@ -327,6 +342,91 @@ func (s *ServerSuite) TestSaveBinlogPath_SaveDroppedSegment() {
}
}
func (s *ServerSuite) TestSaveBinlogPath_TextRequiresStorageV3Manifest() {
s.testServer.meta.AddCollection(&collectionInfo{
ID: 0,
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, DataType: schemapb.DataType_Text},
},
},
})
err := s.testServer.meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 10,
CollectionID: 0,
PartitionID: 1,
InsertChannel: "ch1",
State: commonpb.SegmentState_Sealed,
Level: datapb.SegmentLevel_L1,
NumOfRows: 1,
}))
s.Require().NoError(err)
resp, err := s.testServer.SaveBinlogPaths(context.Background(), &datapb.SaveBinlogPathsRequest{
Base: &commonpb.MsgBase{Timestamp: uint64(time.Now().Unix())},
SegmentID: 10,
CollectionID: 0,
PartitionID: 1,
Channel: "ch1",
SegLevel: datapb.SegmentLevel_L1,
Flushed: true,
StorageVersion: storage.StorageV2,
})
s.NoError(err)
s.ErrorIs(merr.Error(resp), merr.ErrParameterInvalid)
resp, err = s.testServer.SaveBinlogPaths(context.Background(), &datapb.SaveBinlogPathsRequest{
Base: &commonpb.MsgBase{Timestamp: uint64(time.Now().Unix())},
SegmentID: 10,
CollectionID: 0,
PartitionID: 1,
Channel: "ch1",
SegLevel: datapb.SegmentLevel_L1,
Flushed: true,
StorageVersion: storage.StorageV3,
})
s.NoError(err)
s.ErrorIs(merr.Error(resp), merr.ErrParameterInvalid)
resp, err = s.testServer.SaveBinlogPaths(context.Background(), &datapb.SaveBinlogPathsRequest{
Base: &commonpb.MsgBase{Timestamp: uint64(time.Now().Unix())},
SegmentID: 10,
CollectionID: 0,
PartitionID: 1,
Channel: "ch1",
SegLevel: datapb.SegmentLevel_L1,
Flushed: false,
StorageVersion: storage.StorageV3,
})
s.NoError(err)
s.ErrorIs(merr.Error(resp), merr.ErrParameterInvalid)
err = s.testServer.meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 11,
CollectionID: 0,
PartitionID: 1,
InsertChannel: "ch1",
State: commonpb.SegmentState_Dropped,
Level: datapb.SegmentLevel_L1,
NumOfRows: 1,
}))
s.Require().NoError(err)
resp, err = s.testServer.SaveBinlogPaths(context.Background(), &datapb.SaveBinlogPathsRequest{
Base: &commonpb.MsgBase{Timestamp: uint64(time.Now().Unix())},
SegmentID: 11,
CollectionID: 0,
PartitionID: 1,
Channel: "ch1",
SegLevel: datapb.SegmentLevel_L1,
Flushed: true,
StorageVersion: storage.StorageV2,
})
s.NoError(err)
s.True(merr.Ok(resp))
}
func (s *ServerSuite) TestSaveBinlogPath_L0Segment() {
s.testServer.meta.AddCollection(&collectionInfo{ID: 0})
@@ -173,6 +173,11 @@ type WALAccesser interface {
// Local returns the local services.
Local() Local
// PrepareReleaseManualFlush prepares process-local release handoff.
// Returns false when the current process is not the local flush owner or
// the channel does not need growing-source retention.
PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error)
// RawAppend writes a records to the log.
RawAppend(ctx context.Context, msgs message.MutableMessage, opts ...AppendOption) (*types.AppendResult, error)
@@ -188,6 +188,10 @@ func (n *noopWALAccesser) Local() Local {
return &noopLocal{}
}
func (n *noopWALAccesser) PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
return false, getExpectErr()
}
func (n *noopWALAccesser) RawAppend(ctx context.Context, msgs message.MutableMessage, opts ...AppendOption) (*types.AppendResult, error) {
if err := getExpectErr(); err != nil {
return nil, err
+4
View File
@@ -83,6 +83,10 @@ func (w *walAccesserImpl) Local() Local {
return localServiceImpl{w}
}
func (w *walAccesserImpl) PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
return w.handlerClient.PrepareReleaseManualFlush(ctx, collectionID, vchannel, releaseSegmentIDs)
}
// ControlChannel returns the control channel name of the wal.
func (w *walAccesserImpl) ControlChannel() string {
last, err := w.streamingCoordClient.Assignment().GetLatestAssignments(context.Background())
@@ -192,6 +192,20 @@ func TestReleaseTimeout(t *testing.T) {
}, time.Second, 10*time.Millisecond)
}
func TestWALAccesserPrepareReleaseManualFlush(t *testing.T) {
ctx := context.Background()
w, _, _, handler := createMockWAL(t)
defer w.Close()
handler.EXPECT().
PrepareReleaseManualFlush(mock.Anything, int64(100), vChannel1, []int64{1001}).
Return(true, nil)
prepared, err := w.PrepareReleaseManualFlush(ctx, 100, vChannel1, []int64{1001})
assert.NoError(t, err)
assert.True(t, prepared)
}
func newInsertMessage(vChannel string) message.MutableMessage {
msg, err := message.NewInsertMessageBuilderV1().
WithVChannel(vChannel).
+20
View File
@@ -223,6 +223,14 @@ func StartSyncing(batchSize int64) SegmentAction {
}
}
func AbortSyncing(batchSize int64) SegmentAction {
return func(info *SegmentInfo) {
info.syncingRows -= batchSize
info.bufferRows += batchSize
info.syncingTasks--
}
}
func FinishSyncing(batchSize int64) SegmentAction {
return func(info *SegmentInfo) {
info.flushedRows += batchSize
@@ -249,6 +257,18 @@ func UpdateManifestPath(manifestPath string) SegmentAction {
}
}
// SetFlushSourceMode records which subsystem owns the segment's payload at
// flush time. The decision is sticky: once a non-Unknown mode is set, later
// calls with a different mode are no-ops, so the source for a given segment
// stays consistent across its lifetime.
func SetFlushSourceMode(mode FlushSourceMode) SegmentAction {
return func(info *SegmentInfo) {
if info.flushSourceMode == FlushSourceUnknown {
info.flushSourceMode = mode
}
}
}
// MergeSegmentAction is the util function to merge multiple SegmentActions into one.
func MergeSegmentAction(actions ...SegmentAction) SegmentAction {
return func(info *SegmentInfo) {
@@ -0,0 +1,61 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package metacache
// FlushSourceMode indicates which subsystem is responsible for supplying the
// binlog payload of a segment when the WAL flush task executes.
//
// This is process-local runtime state, in the same family as bufferRows /
// syncingRows / syncingTasks. It is NEVER persisted to etcd or DataCoord. The
// mode is decided the first time a segment receives data and remains sticky
// for the segment's lifetime inside metacache. When the segment is removed
// from metacache (via RemoveSegments after flush/drop), the mode is naturally
// garbage-collected together with the SegmentInfo.
//
// On restart, sealed/flushed segments do not need this field (no growing
// phase remains). Growing segments re-derive the value through the regular
// write-buffer source decision path during WAL replay.
//
// Currently consumed only by the TEXT flush owner so that the writeBuffer
// can route between the live insert payload and an out-of-process growing
// source provider without keeping its own shadow map. The field is generic
// so future large-field offload paths (BM25, vector, etc.) can reuse the
// same machinery.
type FlushSourceMode int32
const (
// FlushSourceUnknown means the source has not yet been decided. New
// segments default to this value until the first insert arrives.
FlushSourceUnknown FlushSourceMode = 0
// FlushSourceWriteBuffer means the WriteBuffer's local insert payload is
// the source of truth for sync tasks of this segment.
FlushSourceWriteBuffer FlushSourceMode = 1
// FlushSourceGrowing means the data lives in an external growing source
// and the WriteBuffer only tracks progress.
FlushSourceGrowing FlushSourceMode = 2
)
func (m FlushSourceMode) String() string {
switch m {
case FlushSourceWriteBuffer:
return "WriteBuffer"
case FlushSourceGrowing:
return "Growing"
default:
return "Unknown"
}
}
+12
View File
@@ -49,6 +49,10 @@ type SegmentInfo struct {
bm25logs []*datapb.FieldBinlog
currentSplit []storagecommon.ColumnGroup
manifestPath string
// flushSourceMode is process-local runtime state; not persisted.
// See FlushSourceMode docs for lifecycle semantics.
flushSourceMode FlushSourceMode
}
func (s *SegmentInfo) SegmentID() int64 {
@@ -134,6 +138,13 @@ func (s *SegmentInfo) ManifestPath() string {
return s.manifestPath
}
// FlushSourceMode returns the sticky decision of which subsystem owns this
// segment's payload at flush time. The value is process-local and not
// persisted; see FlushSourceMode docs for details.
func (s *SegmentInfo) FlushSourceMode() FlushSourceMode {
return s.flushSourceMode
}
func (s *SegmentInfo) Clone() *SegmentInfo {
return &SegmentInfo{
segmentID: s.segmentID,
@@ -156,6 +167,7 @@ func (s *SegmentInfo) Clone() *SegmentInfo {
bm25logs: s.bm25logs,
currentSplit: s.currentSplit,
manifestPath: s.manifestPath,
flushSourceMode: s.flushSourceMode,
}
}
@@ -0,0 +1,621 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package syncmgr
import (
"context"
"fmt"
"path"
"sort"
"sync"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/metrics"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/metautil"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/retry"
"github.com/milvus-io/milvus/pkg/v3/util/timerecord"
)
type GrowingFlushConfig struct {
SegmentBasePath string
PartitionBasePath string
CollectionID int64
PartitionID int64
TextFieldIDs []int64
TextLobPaths []string
ReadVersion int64
}
type GrowingFlushResult struct {
ManifestPath string
NumRows int64
}
type GrowingFlushSource interface {
CurrentOffset() int64
FlushGrowingData(ctx context.Context, startOffset, endOffset int64, config *GrowingFlushConfig) (*GrowingFlushResult, error)
Release()
}
type GrowingFlushSourceCommitter interface {
CommitGrowingFlush(targetOffset int64)
}
type GrowingSourceState int
const (
GrowingSourceUnavailable GrowingSourceState = iota
GrowingSourcePending
GrowingSourceUsable
)
type GrowingSourceProvider interface {
GetGrowingFlushSource(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) (GrowingFlushSource, GrowingSourceState)
}
type GrowingSourceReleaseHandoffSegment struct {
SegmentID int64
TargetOffset int64
}
type GrowingSourceReleaseHandoffProvider interface {
PrepareGrowingSourceReleaseHandoff(ctx context.Context, fenceTs uint64, segments []GrowingSourceReleaseHandoffSegment) error
IsReleaseAllowed(segmentID int64, checkpointTs uint64) bool
IsReleasePrepared(segmentID int64, checkpointTs uint64) bool
MarkReleaseDetached(segmentID int64)
ClearReleasePrepared(segmentID int64)
ReleasePreparedSegments() []int64
}
type GrowingSourceRegistry struct {
mu sync.RWMutex
nextToken uint64
providers map[string]map[uint64]GrowingSourceProvider
}
type GrowingSourceRegistration struct {
registry *GrowingSourceRegistry
channel string
token uint64
}
func NewGrowingSourceRegistry() *GrowingSourceRegistry {
return &GrowingSourceRegistry{
providers: make(map[string]map[uint64]GrowingSourceProvider),
}
}
func (r *GrowingSourceRegistry) Register(channel string, provider GrowingSourceProvider) *GrowingSourceRegistration {
if provider == nil {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
r.nextToken++
token := r.nextToken
if _, ok := r.providers[channel]; !ok {
r.providers[channel] = make(map[uint64]GrowingSourceProvider)
}
r.providers[channel][token] = provider
return &GrowingSourceRegistration{
registry: r,
channel: channel,
token: token,
}
}
func (r *GrowingSourceRegistry) Unregister(registration *GrowingSourceRegistration) {
if registration == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
providers, ok := r.providers[registration.channel]
if !ok {
return
}
delete(providers, registration.token)
if len(providers) == 0 {
delete(r.providers, registration.channel)
}
}
func (r *GrowingSourceRegistry) ProviderCount(channel string) int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.providers[channel])
}
func (r *GrowingSourceRegistry) getProviders(channel string) []GrowingSourceProvider {
r.mu.RLock()
channelProviders := r.providers[channel]
tokens := make([]uint64, 0, len(channelProviders))
for token := range channelProviders {
tokens = append(tokens, token)
}
sort.Slice(tokens, func(i, j int) bool {
return tokens[i] < tokens[j]
})
providers := make([]GrowingSourceProvider, 0, len(tokens))
for _, token := range tokens {
providers = append(providers, channelProviders[token])
}
r.mu.RUnlock()
return providers
}
func (r *GrowingSourceRegistry) PrepareGrowingSourceReleaseHandoff(ctx context.Context, channel string, fenceTs uint64, segments []GrowingSourceReleaseHandoffSegment) error {
handoffProviders := make([]GrowingSourceReleaseHandoffProvider, 0)
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
handoffProviders = append(handoffProviders, handoffProvider)
}
if len(handoffProviders) == 0 {
return merr.WrapErrChannelNotAvailable(channel, "no local growing-source release handoff provider")
}
for _, handoffProvider := range handoffProviders {
if err := handoffProvider.PrepareGrowingSourceReleaseHandoff(ctx, fenceTs, segments); err != nil {
return err
}
}
for _, segment := range segments {
if segment.TargetOffset > 0 {
if !r.IsReleasePrepared(channel, segment.SegmentID, fenceTs) {
return merr.WrapErrSegmentNotFound(segment.SegmentID, "growing-source release handoff source is not prepared")
}
continue
}
if !r.IsReleaseAllowed(channel, segment.SegmentID, fenceTs) {
return merr.WrapErrSegmentNotFound(segment.SegmentID, "growing-source release handoff is not allowed")
}
}
return nil
}
func (r *GrowingSourceRegistry) IsReleaseAllowed(channel string, segmentID int64, checkpointTs uint64) bool {
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
if handoffProvider.IsReleaseAllowed(segmentID, checkpointTs) {
return true
}
}
return false
}
func (r *GrowingSourceRegistry) IsReleasePrepared(channel string, segmentID int64, checkpointTs uint64) bool {
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
if handoffProvider.IsReleasePrepared(segmentID, checkpointTs) {
return true
}
}
return false
}
func (r *GrowingSourceRegistry) ClearReleasePrepared(channel string, segmentID int64) {
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
handoffProvider.ClearReleasePrepared(segmentID)
}
}
func (r *GrowingSourceRegistry) MarkReleaseDetached(channel string, segmentID int64) {
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
handoffProvider.MarkReleaseDetached(segmentID)
}
}
func (r *GrowingSourceRegistry) ReleasePreparedSegments(channel string) []int64 {
var segments []int64
for _, provider := range r.getProviders(channel) {
handoffProvider, ok := provider.(GrowingSourceReleaseHandoffProvider)
if !ok {
continue
}
segments = append(segments, handoffProvider.ReleasePreparedSegments()...)
}
return lo.Uniq(segments)
}
func (r *GrowingSourceRegistry) Resolve(channel string, segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) (GrowingFlushSource, GrowingSourceState) {
hasPending := false
for _, provider := range r.getProviders(channel) {
if provider == nil {
continue
}
source, state := provider.GetGrowingFlushSource(segmentID, targetOffset, endPos)
if source == nil {
continue
}
switch state {
case GrowingSourceUsable:
return source, GrowingSourceUsable
case GrowingSourcePending:
hasPending = true
source.Release()
default:
source.Release()
}
}
if hasPending {
return nil, GrowingSourcePending
}
return nil, GrowingSourceUnavailable
}
var defaultGrowingSourceRegistry = NewGrowingSourceRegistry()
func DefaultGrowingSourceRegistry() *GrowingSourceRegistry {
return defaultGrowingSourceRegistry
}
type GrowingSourceSyncTask struct {
collectionID int64
partitionID int64
segmentID int64
channelName string
startPosition *msgpb.MsgPosition
checkpoint *msgpb.MsgPosition
batchRows int64
targetOffset int64
level datapb.SegmentLevel
isFlush bool
isDrop bool
metacache metacache.MetaCache
metaWriter MetaWriter
schema *schemapb.CollectionSchema
source GrowingFlushSource
chunkManager storage.ChunkManager
manifestPath string
flushedSize int64
writeRetryOpts []retry.Option
failureCallback func(error)
tr *timerecord.TimeRecorder
}
func NewGrowingSourceSyncTask() *GrowingSourceSyncTask {
return new(GrowingSourceSyncTask)
}
func (t *GrowingSourceSyncTask) WithCollectionID(collectionID int64) *GrowingSourceSyncTask {
t.collectionID = collectionID
return t
}
func (t *GrowingSourceSyncTask) WithPartitionID(partitionID int64) *GrowingSourceSyncTask {
t.partitionID = partitionID
return t
}
func (t *GrowingSourceSyncTask) WithSegmentID(segmentID int64) *GrowingSourceSyncTask {
t.segmentID = segmentID
return t
}
func (t *GrowingSourceSyncTask) WithChannelName(channelName string) *GrowingSourceSyncTask {
t.channelName = channelName
return t
}
func (t *GrowingSourceSyncTask) WithStartPosition(position *msgpb.MsgPosition) *GrowingSourceSyncTask {
t.startPosition = position
return t
}
func (t *GrowingSourceSyncTask) WithCheckpoint(position *msgpb.MsgPosition) *GrowingSourceSyncTask {
t.checkpoint = position
return t
}
func (t *GrowingSourceSyncTask) WithBatchRows(batchRows int64) *GrowingSourceSyncTask {
t.batchRows = batchRows
return t
}
func (t *GrowingSourceSyncTask) WithTargetOffset(targetOffset int64) *GrowingSourceSyncTask {
t.targetOffset = targetOffset
return t
}
func (t *GrowingSourceSyncTask) WithLevel(level datapb.SegmentLevel) *GrowingSourceSyncTask {
t.level = level
return t
}
func (t *GrowingSourceSyncTask) WithFlush() *GrowingSourceSyncTask {
t.isFlush = true
return t
}
func (t *GrowingSourceSyncTask) WithDrop() *GrowingSourceSyncTask {
t.isDrop = true
return t
}
func (t *GrowingSourceSyncTask) WithMetaCache(metacache metacache.MetaCache) *GrowingSourceSyncTask {
t.metacache = metacache
return t
}
func (t *GrowingSourceSyncTask) WithMetaWriter(metaWriter MetaWriter) *GrowingSourceSyncTask {
t.metaWriter = metaWriter
return t
}
func (t *GrowingSourceSyncTask) WithSchema(schema *schemapb.CollectionSchema) *GrowingSourceSyncTask {
t.schema = schema
return t
}
func (t *GrowingSourceSyncTask) WithSource(source GrowingFlushSource) *GrowingSourceSyncTask {
t.source = source
return t
}
func (t *GrowingSourceSyncTask) WithChunkManager(cm storage.ChunkManager) *GrowingSourceSyncTask {
t.chunkManager = cm
return t
}
func (t *GrowingSourceSyncTask) WithWriteRetryOptions(opts ...retry.Option) *GrowingSourceSyncTask {
t.writeRetryOpts = opts
return t
}
func (t *GrowingSourceSyncTask) WithFailureCallback(callback func(error)) *GrowingSourceSyncTask {
t.failureCallback = callback
return t
}
func (t *GrowingSourceSyncTask) SegmentID() int64 {
return t.segmentID
}
func (t *GrowingSourceSyncTask) Checkpoint() *msgpb.MsgPosition {
return t.checkpoint
}
func (t *GrowingSourceSyncTask) StartPosition() *msgpb.MsgPosition {
return t.startPosition
}
func (t *GrowingSourceSyncTask) ChannelName() string {
return t.channelName
}
func (t *GrowingSourceSyncTask) IsFlush() bool {
return t.isFlush
}
func (t *GrowingSourceSyncTask) IsDrop() bool {
return t.isDrop
}
func (t *GrowingSourceSyncTask) ManifestPath() string {
return t.manifestPath
}
func (t *GrowingSourceSyncTask) BatchRows() int64 {
return t.batchRows
}
func (t *GrowingSourceSyncTask) TargetOffset() int64 {
return t.targetOffset
}
func (t *GrowingSourceSyncTask) HandleError(err error) {
if t.failureCallback != nil {
t.failureCallback(err)
}
metrics.DataNodeFlushBufferCount.WithLabelValues(paramtable.GetStringNodeID(), metrics.FailLabel, t.level.String()).Inc()
}
func (t *GrowingSourceSyncTask) ReleaseSource() {
if t.source != nil {
t.source.Release()
t.source = nil
}
}
func (t *GrowingSourceSyncTask) Run(ctx context.Context) (err error) {
t.tr = timerecord.NewTimeRecorder("growingSourceSyncTask")
log := log.Ctx(ctx).With(
zap.Int64("collectionID", t.collectionID),
zap.Int64("partitionID", t.partitionID),
zap.Int64("segmentID", t.segmentID),
zap.String("channel", t.channelName),
)
commitSource := false
defer func() {
committer, shouldCommit := t.source.(GrowingFlushSourceCommitter)
t.ReleaseSource()
if commitSource && shouldCommit && (t.IsFlush() || t.IsDrop()) {
committer.CommitGrowingFlush(t.targetOffset)
}
if err != nil {
t.HandleError(err)
}
}()
segment, ok := t.metacache.GetSegmentByID(t.segmentID)
if !ok {
if t.isDrop {
log.Info("segment dropped, discard growing source sync task")
return nil
}
log.Warn("segment not found in metacache")
return nil
}
if t.source == nil {
return errors.New("growing flush source is nil")
}
if t.source.CurrentOffset() < t.targetOffset {
return errors.Errorf("growing flush source is behind target offset, current=%d target=%d", t.source.CurrentOffset(), t.targetOffset)
}
expectedRows := t.targetOffset - segment.FlushedRows()
if expectedRows < 0 {
return errors.Errorf("growing source target offset is behind flushed rows, flushedRows=%d targetOffset=%d segmentID=%d",
segment.FlushedRows(), t.targetOffset, t.segmentID)
}
if expectedRows == 0 {
t.manifestPath = segment.ManifestPath()
} else {
config := t.buildFlushConfig(segment)
result, err := t.source.FlushGrowingData(ctx, segment.FlushedRows(), t.targetOffset, config)
if err != nil {
return errors.Wrap(err, "flush growing source data")
}
if result == nil || result.ManifestPath == "" {
return errors.New("growing source flush returned empty manifest")
}
if result.NumRows != expectedRows {
return errors.Errorf("growing source flush row count mismatch, expected=%d actual=%d flushedRows=%d targetOffset=%d segmentID=%d",
expectedRows, result.NumRows, segment.FlushedRows(), t.targetOffset, t.segmentID)
}
t.manifestPath = result.ManifestPath
}
t.flushedSize = expectedRows
if t.metaWriter != nil {
if err := t.metaWriter.UpdateGrowingSourceSync(ctx, t); err != nil {
return err
}
}
actions := make([]metacache.SegmentAction, 0, 3)
if t.batchRows > 0 {
actions = append(actions, metacache.FinishSyncing(t.batchRows))
}
if t.manifestPath != "" {
actions = append(actions, metacache.UpdateManifestPath(t.manifestPath))
}
if t.IsFlush() {
actions = append(actions, metacache.UpdateState(commonpb.SegmentState_Flushed))
}
t.metacache.UpdateSegments(metacache.MergeSegmentAction(actions...), metacache.WithSegmentIDs(t.segmentID))
if t.isDrop {
t.metacache.RemoveSegments(metacache.WithSegmentIDs(t.segmentID))
log.Info("dropped growing source segment removed")
}
commitSource = true
metrics.DataNodeWriteDataCount.WithLabelValues(paramtable.GetStringNodeID(), metrics.StreamingDataSourceLabel, metrics.InsertLabel, fmt.Sprint(t.collectionID)).Add(float64(t.batchRows))
metrics.DataNodeFlushedRows.WithLabelValues(paramtable.GetStringNodeID(), metrics.StreamingDataSourceLabel).Add(float64(t.batchRows))
metrics.DataNodeFlushBufferCount.WithLabelValues(paramtable.GetStringNodeID(), metrics.SuccessLabel, t.level.String()).Inc()
log.Info("growing source sync task done",
zap.Int64("targetOffset", t.targetOffset),
zap.Int64("batchRows", t.batchRows),
zap.String("manifestPath", t.manifestPath),
zap.Duration("timeTaken", t.tr.ElapseSpan()))
return nil
}
func (t *GrowingSourceSyncTask) buildFlushConfig(segment *metacache.SegmentInfo) *GrowingFlushConfig {
segmentBasePath := path.Join(t.chunkManager.RootPath(), common.SegmentInsertLogPath,
metautil.JoinIDPath(t.collectionID, t.partitionID, t.segmentID))
partitionBasePath := path.Join(t.chunkManager.RootPath(), common.SegmentInsertLogPath,
metautil.JoinIDPath(t.collectionID, t.partitionID))
var textFieldIDs []int64
var textLobPaths []string
if t.schema != nil {
for _, field := range t.schema.GetFields() {
if field.GetDataType() == schemapb.DataType_Text {
fieldID := field.GetFieldID()
textFieldIDs = append(textFieldIDs, fieldID)
textLobPaths = append(textLobPaths, fmt.Sprintf("%s/lobs/%d", partitionBasePath, fieldID))
}
}
}
return &GrowingFlushConfig{
SegmentBasePath: segmentBasePath,
PartitionBasePath: partitionBasePath,
CollectionID: t.collectionID,
PartitionID: t.partitionID,
TextFieldIDs: textFieldIDs,
TextLobPaths: textLobPaths,
ReadVersion: manifestVersion(segment.ManifestPath()),
}
}
func manifestVersion(manifestPath string) int64 {
if manifestPath == "" {
return -1
}
if _, version, err := packedManifestVersion(manifestPath); err == nil {
return version
}
return -1
}
func packedManifestVersion(manifestPath string) (string, int64, error) {
return packed.UnmarshalManifestPath(manifestPath)
}
func (t *GrowingSourceSyncTask) startPositions() []*datapb.SegmentStartPosition {
startPos := lo.Map(t.metacache.GetSegmentsBy(
metacache.WithSegmentState(commonpb.SegmentState_Growing, commonpb.SegmentState_Sealed, commonpb.SegmentState_Flushing),
metacache.WithLevel(datapb.SegmentLevel_L1),
metacache.WithStartPosNotRecorded(),
), func(info *metacache.SegmentInfo, _ int) *datapb.SegmentStartPosition {
return &datapb.SegmentStartPosition{
SegmentID: info.SegmentID(),
StartPosition: info.StartPosition(),
}
})
if t.level == datapb.SegmentLevel_L0 {
startPos = append(startPos, &datapb.SegmentStartPosition{SegmentID: t.segmentID, StartPosition: t.startPosition})
}
return startPos
}
@@ -0,0 +1,91 @@
package syncmgr
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/metacache/pkoracle"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
)
type fakeCommitGrowingFlushSource struct {
commits []int64
}
func (s *fakeCommitGrowingFlushSource) CurrentOffset() int64 {
return 10
}
func (s *fakeCommitGrowingFlushSource) FlushGrowingData(context.Context, int64, int64, *GrowingFlushConfig) (*GrowingFlushResult, error) {
return &GrowingFlushResult{ManifestPath: "manifest", NumRows: 10}, nil
}
func (s *fakeCommitGrowingFlushSource) Release() {
}
func (s *fakeCommitGrowingFlushSource) CommitGrowingFlush(targetOffset int64) {
s.commits = append(s.commits, targetOffset)
}
func TestGrowingSourceSyncTaskCommitRetainedSourceOnlyOnFinalization(t *testing.T) {
run := func(t *testing.T, finalize func(*GrowingSourceSyncTask), expectCommit bool) {
segmentID := int64(1)
mc := metacache.NewMockMetaCache(t)
segment := metacache.NewSegmentInfo(&datapb.SegmentInfo{
ID: segmentID,
PartitionID: 2,
ManifestPath: "manifest",
}, pkoracle.NewBloomFilterSet(), nil)
metacache.UpdateNumOfRows(10)(segment)
source := &fakeCommitGrowingFlushSource{}
mc.EXPECT().GetSegmentByID(segmentID).Return(segment, true)
mc.EXPECT().UpdateSegments(mock.Anything, mock.Anything).Run(func(action metacache.SegmentAction, filters ...metacache.SegmentFilter) {
action(segment)
}).Return()
mc.EXPECT().RemoveSegments(mock.Anything).Return(nil).Maybe()
task := NewGrowingSourceSyncTask().
WithCollectionID(3).
WithPartitionID(2).
WithSegmentID(segmentID).
WithChannelName("ch").
WithStartPosition(&msgpb.MsgPosition{Timestamp: 100}).
WithCheckpoint(&msgpb.MsgPosition{Timestamp: 200}).
WithBatchRows(0).
WithTargetOffset(10).
WithMetaCache(mc).
WithSource(source)
if finalize != nil {
finalize(task)
}
require.NoError(t, task.Run(context.Background()))
if expectCommit {
require.Equal(t, []int64{10}, source.commits)
} else {
require.Empty(t, source.commits)
}
}
t.Run("non_final_sync_does_not_commit_retained_source", func(t *testing.T) {
run(t, nil, false)
})
t.Run("flush_finalization_commits_retained_source", func(t *testing.T) {
run(t, func(task *GrowingSourceSyncTask) {
task.WithFlush()
}, true)
})
t.Run("drop_finalization_commits_retained_source", func(t *testing.T) {
run(t, func(task *GrowingSourceSyncTask) {
task.WithDrop()
}, true)
})
}
@@ -21,6 +21,7 @@ type Task interface {
Run(context.Context) error
HandleError(error)
IsFlush() bool
IsDrop() bool
}
// pendingTask wraps a task queued for execution.
@@ -21,6 +21,7 @@ import (
// MetaWriter is the interface for SyncManager to write segment sync meta.
type MetaWriter interface {
UpdateSync(context.Context, *SyncTask) error
UpdateGrowingSourceSync(context.Context, *GrowingSourceSyncTask) error
DropChannel(context.Context, string) error
}
@@ -162,6 +163,76 @@ func (b *brokerMetaWriter) UpdateSync(ctx context.Context, pack *SyncTask) error
return nil
}
func (b *brokerMetaWriter) UpdateGrowingSourceSync(ctx context.Context, task *GrowingSourceSyncTask) error {
segment, ok := task.metacache.GetSegmentByID(task.segmentID)
if !ok {
return merr.WrapErrSegmentNotFound(task.segmentID)
}
startPos := task.startPositions()
checkPoints := []*datapb.CheckPoint{{
SegmentID: task.segmentID,
NumOfRows: segment.FlushedRows() + task.batchRows,
Position: task.checkpoint,
}}
log.Info("SaveBinlogPath for growing source sync",
zap.Int64("SegmentID", task.segmentID),
zap.Int64("CollectionID", task.collectionID),
zap.Int64("ParitionID", task.partitionID),
zap.Any("startPos", startPos),
zap.Any("checkPoints", checkPoints),
zap.String("manifestPath", task.manifestPath),
zap.String("vChannelName", task.channelName),
)
req := &datapb.SaveBinlogPathsRequest{
Base: commonpbutil.NewMsgBase(
commonpbutil.WithMsgType(0),
commonpbutil.WithMsgID(0),
commonpbutil.WithSourceID(b.serverID),
),
SegmentID: task.segmentID,
CollectionID: task.collectionID,
PartitionID: task.partitionID,
CheckPoints: checkPoints,
StartPositions: startPos,
Flushed: task.IsFlush(),
Dropped: task.IsDrop(),
Channel: task.channelName,
SegLevel: task.level,
StorageVersion: storage.StorageV3,
WithFullBinlogs: false,
ManifestPath: task.manifestPath,
}
err := retry.Handle(ctx, func() (bool, error) {
err := b.broker.SaveBinlogPaths(ctx, req)
if errors.IsAny(err, merr.ErrSegmentNotFound, merr.ErrChannelNotFound) {
log.Warn("meta error found, fail growing source sync",
zap.String("channel", task.channelName),
zap.Int64("segmentID", task.segmentID),
zap.Error(err))
return false, err
}
if err != nil {
return !merr.IsCanceledOrTimeout(err), err
}
return false, nil
}, b.opts...)
if err != nil {
log.Warn("failed to SaveBinlogPaths for growing source sync",
zap.Int64("segmentID", task.segmentID),
zap.Error(err))
return err
}
task.metacache.UpdateSegments(metacache.SetStartPosRecorded(true), metacache.WithSegmentIDs(lo.Map(startPos, func(pos *datapb.SegmentStartPosition, _ int) int64 {
return pos.GetSegmentID()
})...))
return nil
}
func (b *brokerMetaWriter) DropChannel(ctx context.Context, channelName string) error {
err := retry.Handle(ctx, func() (bool, error) {
status, err := b.broker.DropVirtualChannel(context.Background(), &datapb.DropVirtualChannelRequest{
@@ -8,10 +8,12 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/internal/flushcommon/broker"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/metacache/pkoracle"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/retry"
)
@@ -100,6 +102,55 @@ func (s *MetaWriterSuite) TestReturnError() {
s.Error(err)
}
func (s *MetaWriterSuite) TestGrowingSourceSyncMetaErrorsReturnError() {
testCases := []struct {
name string
err error
targetErr error
}{
{
name: "segment_not_found",
err: merr.WrapErrSegmentNotFound(1),
targetErr: merr.ErrSegmentNotFound,
},
{
name: "channel_not_found",
err: merr.WrapErrChannelNotFound("ch"),
targetErr: merr.ErrChannelNotFound,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bfs := pkoracle.NewBloomFilterSet()
seg := metacache.NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
PartitionID: 2,
}, bfs, nil)
s.metacache.EXPECT().GetSegmentByID(int64(1)).Return(seg, true).Once()
s.metacache.EXPECT().GetSegmentsBy(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
s.broker.EXPECT().SaveBinlogPaths(mock.Anything, mock.Anything).Return(tc.err).Once()
task := NewGrowingSourceSyncTask().
WithCollectionID(3).
WithPartitionID(2).
WithSegmentID(1).
WithChannelName("ch").
WithCheckpoint(&msgpb.MsgPosition{Timestamp: 100}).
WithBatchRows(10).
WithTargetOffset(10).
WithMetaCache(s.metacache)
task.manifestPath = "manifest"
err := s.writer.UpdateGrowingSourceSync(ctx, task)
s.ErrorIs(err, tc.targetErr)
})
}
}
func TestMetaWriter(t *testing.T) {
suite.Run(t, new(MetaWriterSuite))
}
@@ -115,6 +115,53 @@ func (_c *MockMetaWriter_UpdateSync_Call) RunAndReturn(run func(context.Context,
return _c
}
// UpdateGrowingSourceSync provides a mock function with given fields: _a0, _a1
func (_m *MockMetaWriter) UpdateGrowingSourceSync(_a0 context.Context, _a1 *GrowingSourceSyncTask) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for UpdateGrowingSourceSync")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *GrowingSourceSyncTask) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockMetaWriter_UpdateGrowingSourceSync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateGrowingSourceSync'
type MockMetaWriter_UpdateGrowingSourceSync_Call struct {
*mock.Call
}
// UpdateGrowingSourceSync is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 *GrowingSourceSyncTask
func (_e *MockMetaWriter_Expecter) UpdateGrowingSourceSync(_a0 interface{}, _a1 interface{}) *MockMetaWriter_UpdateGrowingSourceSync_Call {
return &MockMetaWriter_UpdateGrowingSourceSync_Call{Call: _e.mock.On("UpdateGrowingSourceSync", _a0, _a1)}
}
func (_c *MockMetaWriter_UpdateGrowingSourceSync_Call) Run(run func(_a0 context.Context, _a1 *GrowingSourceSyncTask)) *MockMetaWriter_UpdateGrowingSourceSync_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*GrowingSourceSyncTask))
})
return _c
}
func (_c *MockMetaWriter_UpdateGrowingSourceSync_Call) Return(_a0 error) *MockMetaWriter_UpdateGrowingSourceSync_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockMetaWriter_UpdateGrowingSourceSync_Call) RunAndReturn(run func(context.Context, *GrowingSourceSyncTask) error) *MockMetaWriter_UpdateGrowingSourceSync_Call {
_c.Call.Return(run)
return _c
}
// NewMockMetaWriter creates a new instance of MockMetaWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockMetaWriter(t interface {
+45
View File
@@ -147,6 +147,51 @@ func (_c *MockTask_HandleError_Call) RunAndReturn(run func(error)) *MockTask_Han
return _c
}
// IsDrop provides a mock function with no fields
func (_m *MockTask) IsDrop() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsDrop")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockTask_IsDrop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDrop'
type MockTask_IsDrop_Call struct {
*mock.Call
}
// IsDrop is a helper method to define mock.On call
func (_e *MockTask_Expecter) IsDrop() *MockTask_IsDrop_Call {
return &MockTask_IsDrop_Call{Call: _e.mock.On("IsDrop")}
}
func (_c *MockTask_IsDrop_Call) Run(run func()) *MockTask_IsDrop_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockTask_IsDrop_Call) Return(_a0 bool) *MockTask_IsDrop_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockTask_IsDrop_Call) RunAndReturn(run func() bool) *MockTask_IsDrop_Call {
_c.Call.Return(run)
return _c
}
// IsFlush provides a mock function with no fields
func (_m *MockTask) IsFlush() bool {
ret := _m.Called()
@@ -121,6 +121,8 @@ func (mgr *syncManager) SyncData(ctx context.Context, task Task, callbacks ...fu
switch t := task.(type) {
case *SyncTask:
t.WithChunkManager(mgr.chunkManager)
case *GrowingSourceSyncTask:
t.WithChunkManager(mgr.chunkManager)
}
return mgr.safeSubmitTask(ctx, task, callbacks...), nil
@@ -134,6 +136,8 @@ func (mgr *syncManager) SyncDataWithChunkManager(ctx context.Context, task Task,
switch t := task.(type) {
case *SyncTask:
t.WithChunkManager(chunkManager)
case *GrowingSourceSyncTask:
t.WithChunkManager(chunkManager)
}
return mgr.safeSubmitTask(ctx, task, callbacks...), nil
+4
View File
@@ -306,6 +306,10 @@ func (t *SyncTask) IsFlush() bool {
return t.pack.isFlush
}
func (t *SyncTask) IsDrop() bool {
return t.pack.isDrop
}
func (t *SyncTask) Binlogs() (map[int64]*datapb.FieldBinlog, map[int64]*datapb.FieldBinlog, *datapb.FieldBinlog, map[int64]*datapb.FieldBinlog) {
return t.insertBinlogs, t.statsBinlogs, t.deltaBinlog, t.bm25Binlogs
}
@@ -63,17 +63,20 @@ func (wb *l0WriteBuffer) BufferData(insertData []*InsertData, deleteMsgs []*msgs
wb.mut.Lock()
defer wb.mut.Unlock()
// For TEXT collections, skip Insert buffer here.
// Insert data will be flushed by QueryNode's Growing Segment (via GrowingFlushManager).
// This avoids duplicate flush paths for TEXT data.
if !wb.hasTextFields {
// buffer insert data and add segment if not exists
for _, inData := range insertData {
err := wb.bufferInsert(inData, startPos, endPos, schemaVersion)
if err != nil {
return err
for _, inData := range insertData {
if wb.useGrowingSourceFlush {
targetOffset := wb.growingSourceTargetOffset(inData.segmentID, inData.rowNum)
decision := wb.decideGrowingFlushSource(inData.segmentID, targetOffset, endPos)
if decision.sourceType == metacache.FlushSourceGrowing {
wb.recordGrowingSourceProgress(inData, startPos, endPos, schemaVersion, targetOffset)
continue
}
}
err := wb.bufferInsert(inData, startPos, endPos, schemaVersion)
if err != nil {
return err
}
}
// In streaming service mode, flushed segments no longer maintain a bloom filter.
@@ -82,6 +85,7 @@ func (wb *l0WriteBuffer) BufferData(insertData []*InsertData, deleteMsgs []*msgs
wb.dispatchDeleteMsgsWithoutFilter(deleteMsgs, startPos, endPos)
// update buffer last checkpoint
wb.checkpoint = endPos
wb.updateProcessedTsLocked(endPos.GetTimestamp())
segmentsSync := wb.triggerSync()
for _, segment := range segmentsSync {
File diff suppressed because it is too large Load Diff
+40 -4
View File
@@ -47,8 +47,11 @@ type BufferManager interface {
// NotifyCheckpointUpdated notify write buffer checkpoint updated to reset flushTs.
NotifyCheckpointUpdated(channel string, ts uint64)
// HasTextFields returns true if the collection on this channel has TEXT fields.
HasTextFields(channel string) bool
// UseGrowingSourceFlush returns true if the collection on this channel has growing-source fields.
UseGrowingSourceFlush(channel string) bool
// GetGrowingFlushProgress returns growing-source progress for the given channel.
// If segmentIDs is empty, all tracked growing-source segments are returned.
GetGrowingFlushProgress(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error)
// Start makes the background check start to work.
Start()
@@ -56,6 +59,10 @@ type BufferManager interface {
Stop()
}
type ReleaseManualFlushNeedChecker interface {
CheckReleaseManualFlushNeed(ctx context.Context, channel string, segmentIDs []int64) (bool, error)
}
// NewManager returns initialized manager as `Manager`
func NewManager(syncMgr syncmgr.SyncManager) BufferManager {
return &bufferManager{
@@ -241,12 +248,41 @@ func (m *bufferManager) BufferData(channel string, insertData []*InsertData, del
return buf.BufferData(insertData, deleteMsgs, startPos, endPos, schemaVersion)
}
func (m *bufferManager) HasTextFields(channel string) bool {
func (m *bufferManager) UseGrowingSourceFlush(channel string) bool {
buf, loaded := m.buffers.Get(channel)
if !loaded {
return false
}
return buf.HasTextFields()
return buf.UseGrowingSourceFlush()
}
func (m *bufferManager) CheckReleaseManualFlushNeed(ctx context.Context, channel string, segmentIDs []int64) (bool, error) {
buf, loaded := m.buffers.Get(channel)
if !loaded {
log.Ctx(ctx).Warn("write buffer not found when checking release manual flush",
zap.String("channel", channel),
zap.Int64s("segmentIDs", segmentIDs))
return true, merr.WrapErrChannelNotFound(channel)
}
checker, ok := buf.(interface {
CheckReleaseManualFlushNeed(segmentIDs []int64) bool
})
if !ok {
return true, nil
}
return checker.CheckReleaseManualFlushNeed(segmentIDs), nil
}
func (m *bufferManager) GetGrowingFlushProgress(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error) {
buf, loaded := m.buffers.Get(channel)
if !loaded {
log.Ctx(ctx).Warn("write buffer not found when get growing flush progress",
zap.String("channel", channel),
zap.Int64s("segmentIDs", segmentIDs),
zap.Uint64("fenceTs", fenceTs))
return nil, merr.WrapErrChannelNotFound(channel)
}
return buf.GetGrowingFlushProgress(ctx, segmentIDs, fenceTs)
}
// GetCheckpoint returns checkpoint for provided channel.
+107 -46
View File
@@ -242,52 +242,6 @@ func (_c *MockBufferManager_FlushChannel_Call) RunAndReturn(run func(context.Con
return _c
}
// HasTextFields provides a mock function with given fields: channel
func (_m *MockBufferManager) HasTextFields(channel string) bool {
ret := _m.Called(channel)
if len(ret) == 0 {
panic("no return value specified for HasTextFields")
}
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channel)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockBufferManager_HasTextFields_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasTextFields'
type MockBufferManager_HasTextFields_Call struct {
*mock.Call
}
// HasTextFields is a helper method to define mock.On call
// - channel string
func (_e *MockBufferManager_Expecter) HasTextFields(channel interface{}) *MockBufferManager_HasTextFields_Call {
return &MockBufferManager_HasTextFields_Call{Call: _e.mock.On("HasTextFields", channel)}
}
func (_c *MockBufferManager_HasTextFields_Call) Run(run func(channel string)) *MockBufferManager_HasTextFields_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBufferManager_HasTextFields_Call) Return(_a0 bool) *MockBufferManager_HasTextFields_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBufferManager_HasTextFields_Call) RunAndReturn(run func(string) bool) *MockBufferManager_HasTextFields_Call {
_c.Call.Return(run)
return _c
}
// GetCheckpoint provides a mock function with given fields: channel
func (_m *MockBufferManager) GetCheckpoint(channel string) (*msgpb.MsgPosition, bool, error) {
ret := _m.Called(channel)
@@ -353,6 +307,113 @@ func (_c *MockBufferManager_GetCheckpoint_Call) RunAndReturn(run func(string) (*
return _c
}
// GetGrowingFlushProgress provides a mock function with given fields: ctx, channel, segmentIDs, fenceTs
func (_m *MockBufferManager) GetGrowingFlushProgress(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error) {
ret := _m.Called(ctx, channel, segmentIDs, fenceTs)
if len(ret) == 0 {
panic("no return value specified for GetGrowingFlushProgress")
}
var r0 []GrowingFlushSegmentProgress
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, []int64, uint64) ([]GrowingFlushSegmentProgress, error)); ok {
return rf(ctx, channel, segmentIDs, fenceTs)
}
if rf, ok := ret.Get(0).(func(context.Context, string, []int64, uint64) []GrowingFlushSegmentProgress); ok {
r0 = rf(ctx, channel, segmentIDs, fenceTs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]GrowingFlushSegmentProgress)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, []int64, uint64) error); ok {
r1 = rf(ctx, channel, segmentIDs, fenceTs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBufferManager_GetGrowingFlushProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetGrowingFlushProgress'
type MockBufferManager_GetGrowingFlushProgress_Call struct {
*mock.Call
}
// GetGrowingFlushProgress is a helper method to define mock.On call
// - ctx context.Context
// - channel string
// - segmentIDs []int64
// - fenceTs uint64
func (_e *MockBufferManager_Expecter) GetGrowingFlushProgress(ctx interface{}, channel interface{}, segmentIDs interface{}, fenceTs interface{}) *MockBufferManager_GetGrowingFlushProgress_Call {
return &MockBufferManager_GetGrowingFlushProgress_Call{Call: _e.mock.On("GetGrowingFlushProgress", ctx, channel, segmentIDs, fenceTs)}
}
func (_c *MockBufferManager_GetGrowingFlushProgress_Call) Run(run func(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64)) *MockBufferManager_GetGrowingFlushProgress_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].([]int64), args[3].(uint64))
})
return _c
}
func (_c *MockBufferManager_GetGrowingFlushProgress_Call) Return(_a0 []GrowingFlushSegmentProgress, _a1 error) *MockBufferManager_GetGrowingFlushProgress_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBufferManager_GetGrowingFlushProgress_Call) RunAndReturn(run func(context.Context, string, []int64, uint64) ([]GrowingFlushSegmentProgress, error)) *MockBufferManager_GetGrowingFlushProgress_Call {
_c.Call.Return(run)
return _c
}
// UseGrowingSourceFlush provides a mock function with given fields: channel
func (_m *MockBufferManager) UseGrowingSourceFlush(channel string) bool {
ret := _m.Called(channel)
if len(ret) == 0 {
panic("no return value specified for UseGrowingSourceFlush")
}
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channel)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockBufferManager_UseGrowingSourceFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseGrowingSourceFlush'
type MockBufferManager_UseGrowingSourceFlush_Call struct {
*mock.Call
}
// UseGrowingSourceFlush is a helper method to define mock.On call
// - channel string
func (_e *MockBufferManager_Expecter) UseGrowingSourceFlush(channel interface{}) *MockBufferManager_UseGrowingSourceFlush_Call {
return &MockBufferManager_UseGrowingSourceFlush_Call{Call: _e.mock.On("UseGrowingSourceFlush", channel)}
}
func (_c *MockBufferManager_UseGrowingSourceFlush_Call) Run(run func(channel string)) *MockBufferManager_UseGrowingSourceFlush_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBufferManager_UseGrowingSourceFlush_Call) Return(_a0 bool) *MockBufferManager_UseGrowingSourceFlush_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBufferManager_UseGrowingSourceFlush_Call) RunAndReturn(run func(string) bool) *MockBufferManager_UseGrowingSourceFlush_Call {
_c.Call.Return(run)
return _c
}
// NotifyCheckpointUpdated provides a mock function with given fields: channel, ts
func (_m *MockBufferManager) NotifyCheckpointUpdated(channel string, ts uint64) {
_m.Called(channel, ts)
@@ -223,51 +223,6 @@ func (_c *MockWriteBuffer_EvictBuffer_Call) RunAndReturn(run func(...SyncPolicy)
return _c
}
// HasTextFields provides a mock function with no fields
func (_m *MockWriteBuffer) HasTextFields() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for HasTextFields")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockWriteBuffer_HasTextFields_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasTextFields'
type MockWriteBuffer_HasTextFields_Call struct {
*mock.Call
}
// HasTextFields is a helper method to define mock.On call
func (_e *MockWriteBuffer_Expecter) HasTextFields() *MockWriteBuffer_HasTextFields_Call {
return &MockWriteBuffer_HasTextFields_Call{Call: _e.mock.On("HasTextFields")}
}
func (_c *MockWriteBuffer_HasTextFields_Call) Run(run func()) *MockWriteBuffer_HasTextFields_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockWriteBuffer_HasTextFields_Call) Return(_a0 bool) *MockWriteBuffer_HasTextFields_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWriteBuffer_HasTextFields_Call) RunAndReturn(run func() bool) *MockWriteBuffer_HasTextFields_Call {
_c.Call.Return(run)
return _c
}
// GetCheckpoint provides a mock function with no fields
func (_m *MockWriteBuffer) GetCheckpoint() *msgpb.MsgPosition {
ret := _m.Called()
@@ -360,6 +315,66 @@ func (_c *MockWriteBuffer_GetFlushTimestamp_Call) RunAndReturn(run func() uint64
return _c
}
// GetGrowingFlushProgress provides a mock function with given fields: ctx, segmentIDs, fenceTs
func (_m *MockWriteBuffer) GetGrowingFlushProgress(ctx context.Context, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error) {
ret := _m.Called(ctx, segmentIDs, fenceTs)
if len(ret) == 0 {
panic("no return value specified for GetGrowingFlushProgress")
}
var r0 []GrowingFlushSegmentProgress
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []int64, uint64) ([]GrowingFlushSegmentProgress, error)); ok {
return rf(ctx, segmentIDs, fenceTs)
}
if rf, ok := ret.Get(0).(func(context.Context, []int64, uint64) []GrowingFlushSegmentProgress); ok {
r0 = rf(ctx, segmentIDs, fenceTs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]GrowingFlushSegmentProgress)
}
}
if rf, ok := ret.Get(1).(func(context.Context, []int64, uint64) error); ok {
r1 = rf(ctx, segmentIDs, fenceTs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockWriteBuffer_GetGrowingFlushProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetGrowingFlushProgress'
type MockWriteBuffer_GetGrowingFlushProgress_Call struct {
*mock.Call
}
// GetGrowingFlushProgress is a helper method to define mock.On call
// - ctx context.Context
// - segmentIDs []int64
// - fenceTs uint64
func (_e *MockWriteBuffer_Expecter) GetGrowingFlushProgress(ctx interface{}, segmentIDs interface{}, fenceTs interface{}) *MockWriteBuffer_GetGrowingFlushProgress_Call {
return &MockWriteBuffer_GetGrowingFlushProgress_Call{Call: _e.mock.On("GetGrowingFlushProgress", ctx, segmentIDs, fenceTs)}
}
func (_c *MockWriteBuffer_GetGrowingFlushProgress_Call) Run(run func(ctx context.Context, segmentIDs []int64, fenceTs uint64)) *MockWriteBuffer_GetGrowingFlushProgress_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].([]int64), args[2].(uint64))
})
return _c
}
func (_c *MockWriteBuffer_GetGrowingFlushProgress_Call) Return(_a0 []GrowingFlushSegmentProgress, _a1 error) *MockWriteBuffer_GetGrowingFlushProgress_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockWriteBuffer_GetGrowingFlushProgress_Call) RunAndReturn(run func(context.Context, []int64, uint64) ([]GrowingFlushSegmentProgress, error)) *MockWriteBuffer_GetGrowingFlushProgress_Call {
_c.Call.Return(run)
return _c
}
// HasSegment provides a mock function with given fields: segmentID
func (_m *MockWriteBuffer) HasSegment(segmentID int64) bool {
ret := _m.Called(segmentID)
@@ -406,6 +421,51 @@ func (_c *MockWriteBuffer_HasSegment_Call) RunAndReturn(run func(int64) bool) *M
return _c
}
// UseGrowingSourceFlush provides a mock function with no fields
func (_m *MockWriteBuffer) UseGrowingSourceFlush() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for UseGrowingSourceFlush")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockWriteBuffer_UseGrowingSourceFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseGrowingSourceFlush'
type MockWriteBuffer_UseGrowingSourceFlush_Call struct {
*mock.Call
}
// UseGrowingSourceFlush is a helper method to define mock.On call
func (_e *MockWriteBuffer_Expecter) UseGrowingSourceFlush() *MockWriteBuffer_UseGrowingSourceFlush_Call {
return &MockWriteBuffer_UseGrowingSourceFlush_Call{Call: _e.mock.On("UseGrowingSourceFlush")}
}
func (_c *MockWriteBuffer_UseGrowingSourceFlush_Call) Run(run func()) *MockWriteBuffer_UseGrowingSourceFlush_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockWriteBuffer_UseGrowingSourceFlush_Call) Return(_a0 bool) *MockWriteBuffer_UseGrowingSourceFlush_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWriteBuffer_UseGrowingSourceFlush_Call) RunAndReturn(run func() bool) *MockWriteBuffer_UseGrowingSourceFlush_Call {
_c.Call.Return(run)
return _c
}
// MemorySize provides a mock function with no fields
func (_m *MockWriteBuffer) MemorySize() int64 {
ret := _m.Called()
@@ -3,6 +3,7 @@ package writebuffer
import (
"time"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/internal/allocator"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
@@ -13,6 +14,12 @@ type WriteBufferOption func(opt *writeBufferOption)
type TaskObserverCallback func(t syncmgr.Task, err error)
// GrowingSourceResolver resolves an optional in-memory growing segment source
// for growing-source flush. GrowingSourcePending means the growing source exists but has not
// caught up to targetOffset yet; WriteBuffer should only be used when the state
// is GrowingSourceUnavailable.
type GrowingSourceResolver func(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) (syncmgr.GrowingFlushSource, syncmgr.GrowingSourceState)
type writeBufferOption struct {
idAllocator allocator.Interface
syncPolicies []SyncPolicy
@@ -22,6 +29,9 @@ type writeBufferOption struct {
errorHandler func(error)
taskObserverCallback TaskObserverCallback
storageVersion int64
growingSourceResolver GrowingSourceResolver
growingSourceRetryInterval time.Duration
}
func defaultWBOption(metacache metacache.MetaCache) *writeBufferOption {
@@ -76,3 +86,9 @@ func WithTaskObserverCallback(callback TaskObserverCallback) WriteBufferOption {
opt.taskObserverCallback = callback
}
}
func WithGrowingSourceResolver(resolver GrowingSourceResolver) WriteBufferOption {
return func(opt *writeBufferOption) {
opt.growingSourceResolver = resolver
}
}
File diff suppressed because it is too large Load Diff
@@ -17,12 +17,14 @@ import (
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/metacache/pkoracle"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/mq/msgstream"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/conc"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
)
type WriteBufferSuite struct {
@@ -74,6 +76,100 @@ func (s *WriteBufferSuite) TestHasSegment() {
s.True(s.wb.HasSegment(segmentID))
}
func (s *WriteBufferSuite) TestCreateNewGrowingSegmentStorageVersion() {
param := paramtable.Get()
param.Save(param.CommonCfg.UseLoonFFI.Key, "false")
defer param.Reset(param.CommonCfg.UseLoonFFI.Key)
param.Save(param.CommonCfg.EnableGrowingSourceFlush.Key, "false")
defer param.Reset(param.CommonCfg.EnableGrowingSourceFlush.Key)
s.Run("non_text_uses_v2_when_ffi_disabled", func() {
s.wb.useGrowingSourceFlush = false
s.False(s.wb.UseGrowingSourceFlush())
s.metacache.EXPECT().GetSegmentByID(int64(2001)).Return(nil, false).Once()
s.metacache.EXPECT().AddSegment(mock.MatchedBy(func(info *datapb.SegmentInfo) bool {
return info.GetStorageVersion() == storage.StorageV2 &&
info.GetManifestPath() == "" &&
info.GetSchemaVersion() == 11
}), mock.Anything, mock.Anything, mock.Anything).Return().Once()
s.wb.CreateNewGrowingSegment(10, 2001, nil, 11)
})
s.Run("growing_source_does_not_force_v3_manifest_when_ffi_disabled", func() {
s.wb.useGrowingSourceFlush = true
s.metacache.EXPECT().GetSegmentByID(int64(2002)).Return(nil, false).Once()
s.metacache.EXPECT().AddSegment(mock.MatchedBy(func(info *datapb.SegmentInfo) bool {
return info.GetStorageVersion() == storage.StorageV2 &&
info.GetManifestPath() == "" &&
info.GetSchemaVersion() == 12
}), mock.Anything, mock.Anything, mock.Anything).Return().Once()
s.wb.CreateNewGrowingSegment(10, 2002, nil, 12)
})
s.Run("text_schema_does_not_enable_growing_source_when_ffi_disabled", func() {
textSchema := &schemapb.CollectionSchema{
Name: "wb_text_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64, IsPrimaryKey: true, Name: "pk"},
{FieldID: 101, DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{
{Key: common.DimKey, Value: "128"},
}},
{FieldID: 102, DataType: schemapb.DataType_Text, Name: "text"},
},
}
mc := metacache.NewMockMetaCache(s.T())
mc.EXPECT().GetSchema(mock.Anything).Return(textSchema).Maybe()
mc.EXPECT().Collection().Return(s.collID).Maybe()
wb, err := newWriteBufferBase(s.channelName, mc, s.syncMgr, &writeBufferOption{})
s.Require().NoError(err)
s.False(wb.UseGrowingSourceFlush())
mc.EXPECT().GetSegmentByID(int64(2003)).Return(nil, false).Once()
mc.EXPECT().AddSegment(mock.MatchedBy(func(info *datapb.SegmentInfo) bool {
return info.GetStorageVersion() == storage.StorageV2 &&
info.GetManifestPath() == "" &&
info.GetSchemaVersion() == 13
}), mock.Anything, mock.Anything, mock.Anything).Return().Once()
wb.CreateNewGrowingSegment(10, 2003, nil, 13)
})
s.Run("text_schema_uses_v3_manifest_when_ffi_enabled", func() {
param.Save(param.CommonCfg.UseLoonFFI.Key, "true")
defer param.Save(param.CommonCfg.UseLoonFFI.Key, "false")
textSchema := &schemapb.CollectionSchema{
Name: "wb_text_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64, IsPrimaryKey: true, Name: "pk"},
{FieldID: 101, DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{
{Key: common.DimKey, Value: "128"},
}},
{FieldID: 102, DataType: schemapb.DataType_Text, Name: "text"},
},
}
mc := metacache.NewMockMetaCache(s.T())
mc.EXPECT().GetSchema(mock.Anything).Return(textSchema).Maybe()
mc.EXPECT().Collection().Return(s.collID).Maybe()
wb, err := newWriteBufferBase(s.channelName, mc, s.syncMgr, &writeBufferOption{})
s.Require().NoError(err)
s.True(wb.UseGrowingSourceFlush())
mc.EXPECT().GetSegmentByID(int64(2004)).Return(nil, false).Once()
mc.EXPECT().AddSegment(mock.MatchedBy(func(info *datapb.SegmentInfo) bool {
return info.GetStorageVersion() == storage.StorageV3 &&
info.GetManifestPath() != "" &&
info.GetSchemaVersion() == 14
}), mock.Anything, mock.Anything, mock.Anything).Return().Once()
wb.CreateNewGrowingSegment(10, 2004, nil, 14)
})
}
func (s *WriteBufferSuite) TestFlushSegments() {
segmentID := int64(1001)
@@ -90,7 +186,7 @@ func (s *WriteBufferSuite) TestSealSegmentsMissingSegment() {
segmentID := int64(1001)
s.Run("non_text_returns_error", func() {
s.wb.hasTextFields = false
s.wb.useGrowingSourceFlush = false
s.metacache.EXPECT().GetSegmentByID(segmentID).Return(nil, false).Once()
err := s.wb.SealSegments(context.Background(), []int64{segmentID})
@@ -98,9 +194,9 @@ func (s *WriteBufferSuite) TestSealSegmentsMissingSegment() {
})
s.Run("text_skips_missing_segment", func() {
s.wb.hasTextFields = true
s.wb.useGrowingSourceFlush = true
defer func() {
s.wb.hasTextFields = false
s.wb.useGrowingSourceFlush = false
}()
s.metacache.EXPECT().GetSegmentByID(segmentID).Return(nil, false).Once()
@@ -436,6 +532,97 @@ func (s *WriteBufferSuite) TestEvictBuffer() {
})
}
func (s *WriteBufferSuite) TestGrowingSourceProgressSelectedByPolicy() {
paramtable.Get().Save(paramtable.Get().DataNodeCfg.SyncPeriod.Key, "1")
defer paramtable.Get().Reset(paramtable.Get().DataNodeCfg.SyncPeriod.Key)
paramtable.Get().Save(paramtable.Get().DataNodeCfg.FlushInsertBufferSize.Key, "100")
defer paramtable.Get().Reset(paramtable.Get().DataNodeCfg.FlushInsertBufferSize.Key)
originalEstSize := s.wb.estSizePerRecord
s.wb.estSizePerRecord = 10
defer func() {
s.wb.estSizePerRecord = originalEstSize
}()
now := time.Now()
recentTs := tsoutil.ComposeTSByTime(now.Add(500*time.Millisecond), 0)
staleTs := tsoutil.ComposeTSByTime(now.Add(2*time.Second), 0)
startTs := tsoutil.ComposeTSByTime(now, 0)
s.Run("pending_flush", func() {
selected := s.wb.growingSourceProgressSelectedByPolicy(recentTs, 1001, &growingSourceProgress{
segmentID: 1001,
pendingFlush: true,
})
s.True(selected)
})
s.Run("sealed_segment", func() {
segment := metacache.NewSegmentInfo(&datapb.SegmentInfo{
ID: 1002,
State: commonpb.SegmentState_Sealed,
}, nil, nil)
s.metacache.EXPECT().GetSegmentByID(int64(1002)).Return(segment, true).Once()
selected := s.wb.growingSourceProgressSelectedByPolicy(recentTs, 1002, &growingSourceProgress{
segmentID: 1002,
})
s.True(selected)
})
s.Run("recent_progress", func() {
s.metacache.EXPECT().GetSegmentByID(int64(1003)).Return(nil, false).Once()
selected := s.wb.growingSourceProgressSelectedByPolicy(recentTs, 1003, &growingSourceProgress{
segmentID: 1003,
batches: []growingSourceProgressBatch{
{startPosition: &msgpb.MsgPosition{Timestamp: startTs}},
},
})
s.False(selected)
})
s.Run("below_row_threshold", func() {
segment := metacache.NewSegmentInfo(&datapb.SegmentInfo{
ID: 1004,
}, nil, nil)
s.metacache.EXPECT().GetSegmentByID(int64(1004)).Return(segment, true).Once()
selected := s.wb.growingSourceProgressSelectedByPolicy(recentTs, 1004, &growingSourceProgress{
segmentID: 1004,
targetOffset: 9,
batches: []growingSourceProgressBatch{
{startPosition: &msgpb.MsgPosition{Timestamp: startTs}},
},
})
s.False(selected)
})
s.Run("row_threshold", func() {
segment := metacache.NewSegmentInfo(&datapb.SegmentInfo{
ID: 1005,
}, nil, nil)
s.metacache.EXPECT().GetSegmentByID(int64(1005)).Return(segment, true).Once()
selected := s.wb.growingSourceProgressSelectedByPolicy(recentTs, 1005, &growingSourceProgress{
segmentID: 1005,
targetOffset: 10,
})
s.True(selected)
})
s.Run("stale_progress", func() {
s.metacache.EXPECT().GetSegmentByID(int64(1006)).Return(nil, false).Once()
selected := s.wb.growingSourceProgressSelectedByPolicy(staleTs, 1006, &growingSourceProgress{
segmentID: 1006,
batches: []growingSourceProgressBatch{
{startPosition: &msgpb.MsgPosition{Timestamp: startTs}},
},
})
s.True(selected)
})
}
func (s *WriteBufferSuite) TestDropPartitions() {
wb, err := newWriteBufferBase(s.channelName, s.metacache, s.syncMgr, &writeBufferOption{
pkStatsFactory: func(vchannel *datapb.SegmentInfo) pkoracle.PkStat {
@@ -27,7 +27,7 @@ func (_m *MockWALAccesser) EXPECT() *MockWALAccesser_Expecter {
}
// AppendMessages provides a mock function with given fields: ctx, msgs
func (_m *MockWALAccesser) AppendMessages(ctx context.Context, msgs ...message.MutableMessage) streaming.AppendResponses {
func (_m *MockWALAccesser) AppendMessages(ctx context.Context, msgs ...message.MutableMessage) types.AppendResponses {
_va := make([]interface{}, len(msgs))
for _i := range msgs {
_va[_i] = msgs[_i]
@@ -41,11 +41,11 @@ func (_m *MockWALAccesser) AppendMessages(ctx context.Context, msgs ...message.M
panic("no return value specified for AppendMessages")
}
var r0 streaming.AppendResponses
if rf, ok := ret.Get(0).(func(context.Context, ...message.MutableMessage) streaming.AppendResponses); ok {
var r0 types.AppendResponses
if rf, ok := ret.Get(0).(func(context.Context, ...message.MutableMessage) types.AppendResponses); ok {
r0 = rf(ctx, msgs...)
} else {
r0 = ret.Get(0).(streaming.AppendResponses)
r0 = ret.Get(0).(types.AppendResponses)
}
return r0
@@ -77,12 +77,12 @@ func (_c *MockWALAccesser_AppendMessages_Call) Run(run func(ctx context.Context,
return _c
}
func (_c *MockWALAccesser_AppendMessages_Call) Return(_a0 streaming.AppendResponses) *MockWALAccesser_AppendMessages_Call {
func (_c *MockWALAccesser_AppendMessages_Call) Return(_a0 types.AppendResponses) *MockWALAccesser_AppendMessages_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWALAccesser_AppendMessages_Call) RunAndReturn(run func(context.Context, ...message.MutableMessage) streaming.AppendResponses) *MockWALAccesser_AppendMessages_Call {
func (_c *MockWALAccesser_AppendMessages_Call) RunAndReturn(run func(context.Context, ...message.MutableMessage) types.AppendResponses) *MockWALAccesser_AppendMessages_Call {
_c.Call.Return(run)
return _c
}
@@ -320,6 +320,65 @@ func (_c *MockWALAccesser_Local_Call) RunAndReturn(run func() streaming.Local) *
return _c
}
// PrepareReleaseManualFlush provides a mock function with given fields: ctx, collectionID, vchannel, releaseSegmentIDs
func (_m *MockWALAccesser) PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
ret := _m.Called(ctx, collectionID, vchannel, releaseSegmentIDs)
if len(ret) == 0 {
panic("no return value specified for PrepareReleaseManualFlush")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string, []int64) (bool, error)); ok {
return rf(ctx, collectionID, vchannel, releaseSegmentIDs)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string, []int64) bool); ok {
r0 = rf(ctx, collectionID, vchannel, releaseSegmentIDs)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string, []int64) error); ok {
r1 = rf(ctx, collectionID, vchannel, releaseSegmentIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockWALAccesser_PrepareReleaseManualFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrepareReleaseManualFlush'
type MockWALAccesser_PrepareReleaseManualFlush_Call struct {
*mock.Call
}
// PrepareReleaseManualFlush is a helper method to define mock.On call
// - ctx context.Context
// - collectionID int64
// - vchannel string
// - releaseSegmentIDs []int64
func (_e *MockWALAccesser_Expecter) PrepareReleaseManualFlush(ctx interface{}, collectionID interface{}, vchannel interface{}, releaseSegmentIDs interface{}) *MockWALAccesser_PrepareReleaseManualFlush_Call {
return &MockWALAccesser_PrepareReleaseManualFlush_Call{Call: _e.mock.On("PrepareReleaseManualFlush", ctx, collectionID, vchannel, releaseSegmentIDs)}
}
func (_c *MockWALAccesser_PrepareReleaseManualFlush_Call) Run(run func(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64)) *MockWALAccesser_PrepareReleaseManualFlush_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int64), args[2].(string), args[3].([]int64))
})
return _c
}
func (_c *MockWALAccesser_PrepareReleaseManualFlush_Call) Return(_a0 bool, _a1 error) *MockWALAccesser_PrepareReleaseManualFlush_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockWALAccesser_PrepareReleaseManualFlush_Call) RunAndReturn(run func(context.Context, int64, string, []int64) (bool, error)) *MockWALAccesser_PrepareReleaseManualFlush_Call {
_c.Call.Return(run)
return _c
}
// RawAppend provides a mock function with given fields: ctx, msgs, opts
func (_m *MockWALAccesser) RawAppend(ctx context.Context, msgs message.MutableMessage, opts ...streaming.AppendOption) (*types.AppendResult, error) {
_va := make([]interface{}, len(opts))
@@ -5,12 +5,17 @@ package mock_handler
import (
context "context"
consumer "github.com/milvus-io/milvus/internal/streamingnode/client/handler/consumer"
handler "github.com/milvus-io/milvus/internal/streamingnode/client/handler"
mock "github.com/stretchr/testify/mock"
producer "github.com/milvus-io/milvus/internal/streamingnode/client/handler/producer"
types "github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
wal "github.com/milvus-io/milvus/internal/streamingnode/server/wal"
utility "github.com/milvus-io/milvus/internal/streamingnode/server/wal/utility"
)
// MockHandlerClient is an autogenerated mock type for the HandlerClient type
@@ -59,23 +64,23 @@ func (_c *MockHandlerClient_Close_Call) RunAndReturn(run func()) *MockHandlerCli
}
// CreateConsumer provides a mock function with given fields: ctx, opts
func (_m *MockHandlerClient) CreateConsumer(ctx context.Context, opts *handler.ConsumerOptions) (handler.Consumer, error) {
func (_m *MockHandlerClient) CreateConsumer(ctx context.Context, opts *handler.ConsumerOptions) (consumer.Consumer, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for CreateConsumer")
}
var r0 handler.Consumer
var r0 consumer.Consumer
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *handler.ConsumerOptions) (handler.Consumer, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, *handler.ConsumerOptions) (consumer.Consumer, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, *handler.ConsumerOptions) handler.Consumer); ok {
if rf, ok := ret.Get(0).(func(context.Context, *handler.ConsumerOptions) consumer.Consumer); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(handler.Consumer)
r0 = ret.Get(0).(consumer.Consumer)
}
}
@@ -107,34 +112,34 @@ func (_c *MockHandlerClient_CreateConsumer_Call) Run(run func(ctx context.Contex
return _c
}
func (_c *MockHandlerClient_CreateConsumer_Call) Return(_a0 handler.Consumer, _a1 error) *MockHandlerClient_CreateConsumer_Call {
func (_c *MockHandlerClient_CreateConsumer_Call) Return(_a0 consumer.Consumer, _a1 error) *MockHandlerClient_CreateConsumer_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockHandlerClient_CreateConsumer_Call) RunAndReturn(run func(context.Context, *handler.ConsumerOptions) (handler.Consumer, error)) *MockHandlerClient_CreateConsumer_Call {
func (_c *MockHandlerClient_CreateConsumer_Call) RunAndReturn(run func(context.Context, *handler.ConsumerOptions) (consumer.Consumer, error)) *MockHandlerClient_CreateConsumer_Call {
_c.Call.Return(run)
return _c
}
// CreateProducer provides a mock function with given fields: ctx, opts
func (_m *MockHandlerClient) CreateProducer(ctx context.Context, opts *handler.ProducerOptions) (handler.Producer, error) {
func (_m *MockHandlerClient) CreateProducer(ctx context.Context, opts *handler.ProducerOptions) (producer.Producer, error) {
ret := _m.Called(ctx, opts)
if len(ret) == 0 {
panic("no return value specified for CreateProducer")
}
var r0 handler.Producer
var r0 producer.Producer
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *handler.ProducerOptions) (handler.Producer, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, *handler.ProducerOptions) (producer.Producer, error)); ok {
return rf(ctx, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, *handler.ProducerOptions) handler.Producer); ok {
if rf, ok := ret.Get(0).(func(context.Context, *handler.ProducerOptions) producer.Producer); ok {
r0 = rf(ctx, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(handler.Producer)
r0 = ret.Get(0).(producer.Producer)
}
}
@@ -166,12 +171,12 @@ func (_c *MockHandlerClient_CreateProducer_Call) Run(run func(ctx context.Contex
return _c
}
func (_c *MockHandlerClient_CreateProducer_Call) Return(_a0 handler.Producer, _a1 error) *MockHandlerClient_CreateProducer_Call {
func (_c *MockHandlerClient_CreateProducer_Call) Return(_a0 producer.Producer, _a1 error) *MockHandlerClient_CreateProducer_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockHandlerClient_CreateProducer_Call) RunAndReturn(run func(context.Context, *handler.ProducerOptions) (handler.Producer, error)) *MockHandlerClient_CreateProducer_Call {
func (_c *MockHandlerClient_CreateProducer_Call) RunAndReturn(run func(context.Context, *handler.ProducerOptions) (producer.Producer, error)) *MockHandlerClient_CreateProducer_Call {
_c.Call.Return(run)
return _c
}
@@ -234,23 +239,23 @@ func (_c *MockHandlerClient_GetLatestMVCCTimestampIfLocal_Call) RunAndReturn(run
}
// GetReplicateCheckpoint provides a mock function with given fields: ctx, channelName
func (_m *MockHandlerClient) GetReplicateCheckpoint(ctx context.Context, channelName string) (*wal.ReplicateCheckpoint, error) {
func (_m *MockHandlerClient) GetReplicateCheckpoint(ctx context.Context, channelName string) (*utility.ReplicateCheckpoint, error) {
ret := _m.Called(ctx, channelName)
if len(ret) == 0 {
panic("no return value specified for GetReplicateCheckpoint")
}
var r0 *wal.ReplicateCheckpoint
var r0 *utility.ReplicateCheckpoint
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*wal.ReplicateCheckpoint, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) (*utility.ReplicateCheckpoint, error)); ok {
return rf(ctx, channelName)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *wal.ReplicateCheckpoint); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) *utility.ReplicateCheckpoint); ok {
r0 = rf(ctx, channelName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*wal.ReplicateCheckpoint)
r0 = ret.Get(0).(*utility.ReplicateCheckpoint)
}
}
@@ -282,34 +287,34 @@ func (_c *MockHandlerClient_GetReplicateCheckpoint_Call) Run(run func(ctx contex
return _c
}
func (_c *MockHandlerClient_GetReplicateCheckpoint_Call) Return(_a0 *wal.ReplicateCheckpoint, _a1 error) *MockHandlerClient_GetReplicateCheckpoint_Call {
func (_c *MockHandlerClient_GetReplicateCheckpoint_Call) Return(_a0 *utility.ReplicateCheckpoint, _a1 error) *MockHandlerClient_GetReplicateCheckpoint_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockHandlerClient_GetReplicateCheckpoint_Call) RunAndReturn(run func(context.Context, string) (*wal.ReplicateCheckpoint, error)) *MockHandlerClient_GetReplicateCheckpoint_Call {
func (_c *MockHandlerClient_GetReplicateCheckpoint_Call) RunAndReturn(run func(context.Context, string) (*utility.ReplicateCheckpoint, error)) *MockHandlerClient_GetReplicateCheckpoint_Call {
_c.Call.Return(run)
return _c
}
// GetSalvageCheckpoint provides a mock function with given fields: ctx, channelName
func (_m *MockHandlerClient) GetSalvageCheckpoint(ctx context.Context, channelName string) ([]*wal.ReplicateCheckpoint, error) {
func (_m *MockHandlerClient) GetSalvageCheckpoint(ctx context.Context, channelName string) ([]*utility.ReplicateCheckpoint, error) {
ret := _m.Called(ctx, channelName)
if len(ret) == 0 {
panic("no return value specified for GetSalvageCheckpoint")
}
var r0 []*wal.ReplicateCheckpoint
var r0 []*utility.ReplicateCheckpoint
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*wal.ReplicateCheckpoint, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*utility.ReplicateCheckpoint, error)); ok {
return rf(ctx, channelName)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []*wal.ReplicateCheckpoint); ok {
if rf, ok := ret.Get(0).(func(context.Context, string) []*utility.ReplicateCheckpoint); ok {
r0 = rf(ctx, channelName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*wal.ReplicateCheckpoint)
r0 = ret.Get(0).([]*utility.ReplicateCheckpoint)
}
}
@@ -341,12 +346,12 @@ func (_c *MockHandlerClient_GetSalvageCheckpoint_Call) Run(run func(ctx context.
return _c
}
func (_c *MockHandlerClient_GetSalvageCheckpoint_Call) Return(_a0 []*wal.ReplicateCheckpoint, _a1 error) *MockHandlerClient_GetSalvageCheckpoint_Call {
func (_c *MockHandlerClient_GetSalvageCheckpoint_Call) Return(_a0 []*utility.ReplicateCheckpoint, _a1 error) *MockHandlerClient_GetSalvageCheckpoint_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockHandlerClient_GetSalvageCheckpoint_Call) RunAndReturn(run func(context.Context, string) ([]*wal.ReplicateCheckpoint, error)) *MockHandlerClient_GetSalvageCheckpoint_Call {
func (_c *MockHandlerClient_GetSalvageCheckpoint_Call) RunAndReturn(run func(context.Context, string) ([]*utility.ReplicateCheckpoint, error)) *MockHandlerClient_GetSalvageCheckpoint_Call {
_c.Call.Return(run)
return _c
}
@@ -409,6 +414,65 @@ func (_c *MockHandlerClient_GetWALMetricsIfLocal_Call) RunAndReturn(run func(con
return _c
}
// PrepareReleaseManualFlush provides a mock function with given fields: ctx, collectionID, vchannel, releaseSegmentIDs
func (_m *MockHandlerClient) PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
ret := _m.Called(ctx, collectionID, vchannel, releaseSegmentIDs)
if len(ret) == 0 {
panic("no return value specified for PrepareReleaseManualFlush")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string, []int64) (bool, error)); ok {
return rf(ctx, collectionID, vchannel, releaseSegmentIDs)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string, []int64) bool); ok {
r0 = rf(ctx, collectionID, vchannel, releaseSegmentIDs)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string, []int64) error); ok {
r1 = rf(ctx, collectionID, vchannel, releaseSegmentIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockHandlerClient_PrepareReleaseManualFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrepareReleaseManualFlush'
type MockHandlerClient_PrepareReleaseManualFlush_Call struct {
*mock.Call
}
// PrepareReleaseManualFlush is a helper method to define mock.On call
// - ctx context.Context
// - collectionID int64
// - vchannel string
// - releaseSegmentIDs []int64
func (_e *MockHandlerClient_Expecter) PrepareReleaseManualFlush(ctx interface{}, collectionID interface{}, vchannel interface{}, releaseSegmentIDs interface{}) *MockHandlerClient_PrepareReleaseManualFlush_Call {
return &MockHandlerClient_PrepareReleaseManualFlush_Call{Call: _e.mock.On("PrepareReleaseManualFlush", ctx, collectionID, vchannel, releaseSegmentIDs)}
}
func (_c *MockHandlerClient_PrepareReleaseManualFlush_Call) Run(run func(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64)) *MockHandlerClient_PrepareReleaseManualFlush_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int64), args[2].(string), args[3].([]int64))
})
return _c
}
func (_c *MockHandlerClient_PrepareReleaseManualFlush_Call) Return(_a0 bool, _a1 error) *MockHandlerClient_PrepareReleaseManualFlush_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockHandlerClient_PrepareReleaseManualFlush_Call) RunAndReturn(run func(context.Context, int64, string, []int64) (bool, error)) *MockHandlerClient_PrepareReleaseManualFlush_Call {
_c.Call.Return(run)
return _c
}
// NewMockHandlerClient creates a new instance of MockHandlerClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockHandlerClient(t interface {
+21
View File
@@ -205,6 +205,13 @@ type dmlTask interface {
type BaseInsertTask = msgstream.InsertMsg
func validateTextStorageV3Enabled(schema *schemapb.CollectionSchema) error {
if err := typeutil.ValidateTextRequiresStorageV3(schema, Params.CommonCfg.UseLoonFFI.GetAsBool()); err != nil {
return merr.WrapErrParameterInvalidMsg("%s", err.Error())
}
return nil
}
type createCollectionTask struct {
baseTask
Condition
@@ -450,6 +457,9 @@ func (t *createCollectionTask) PreExecute(ctx context.Context) error {
if err := typeutil.NormalizeAndValidateExternalCollectionSchema(t.schema); err != nil {
return err
}
if err := validateTextStorageV3Enabled(t.schema); err != nil {
return err
}
// External collections must be single-shard: the refresh mechanism assigns all
// segments to VChannelNames[0], so multiple shards would leave segments orphaned.
@@ -944,6 +954,11 @@ func validateAddFieldRequest(schema *schemapb.CollectionSchema, newFieldSchema *
return merr.WrapErrParameterInvalidMsg("%s", err.Error())
}
}
schemaWithNewField := proto.Clone(schema).(*schemapb.CollectionSchema)
schemaWithNewField.Fields = append(schemaWithNewField.GetFields(), proto.Clone(newFieldSchema).(*schemapb.FieldSchema))
if err := validateTextStorageV3Enabled(schemaWithNewField); err != nil {
return err
}
if funcutil.SliceContain([]string{common.RowIDFieldName, common.TimeStampFieldName, common.MetaFieldName, common.NamespaceFieldName, common.VirtualPKFieldName}, newFieldSchema.GetName()) {
return merr.WrapErrParameterInvalidMsg(fmt.Sprintf("not support to add system field, field name = %s", newFieldSchema.GetName()))
}
@@ -3086,6 +3101,9 @@ func (t *loadCollectionTask) Execute(ctx context.Context) (err error) {
if err != nil {
return err
}
if err := validateTextStorageV3Enabled(collSchema.CollectionSchema); err != nil {
return err
}
// prepare load field list
loadFields, err := collSchema.GetLoadFieldIDs(t.GetLoadFields(), t.GetSkipLoadDynamicField())
if err != nil {
@@ -3344,6 +3362,9 @@ func (t *loadPartitionsTask) Execute(ctx context.Context) error {
if err != nil {
return err
}
if err := validateTextStorageV3Enabled(collSchema.CollectionSchema); err != nil {
return err
}
// prepare load field list
loadFields, err := collSchema.GetLoadFieldIDs(t.GetLoadFields(), t.GetSkipLoadDynamicField())
if err != nil {
+3
View File
@@ -287,6 +287,9 @@ func (dr *deleteRunner) Init(ctx context.Context) error {
if err != nil {
return ErrWithLog(log, "Failed to get collection schema", err)
}
if err := validateTextStorageV3Enabled(dr.schema.CollectionSchema); err != nil {
return ErrWithLog(log, "TEXT field requires StorageV3", err)
}
colInfo, err := globalMetaCache.GetCollectionInfo(ctx, dr.req.GetDbName(), collName, dr.collectionID)
if err != nil {
+2 -21
View File
@@ -52,19 +52,6 @@ func (t *flushTask) Execute(ctx context.Context) error {
return merr.WrapErrAsInputErrorWhen(err, merr.ErrCollectionNotFound, merr.ErrDatabaseNotFound)
}
// Check if collection has TEXT fields — TEXT collections use deferred flush via WAL.
schemaInfo, err := globalMetaCache.GetCollectionSchema(ctx, t.DbName, collName)
if err != nil {
return err
}
hasTextFields := false
for _, field := range schemaInfo.GetFields() {
if field.GetDataType() == schemapb.DataType_Text {
hasTextFields = true
break
}
}
vchannels, err := t.chMgr.getVChannels(collID)
if err != nil {
return err
@@ -77,9 +64,7 @@ func (t *flushTask) Execute(ctx context.Context) error {
if err != nil {
return err
}
if !hasTextFields {
onFlushSegmentIDs = append(onFlushSegmentIDs, segmentIDs...)
}
onFlushSegmentIDs = append(onFlushSegmentIDs, segmentIDs...)
}
// Ask datacoord to get flushed segment infos.
@@ -108,11 +93,7 @@ func (t *flushTask) Execute(ctx context.Context) error {
coll2Segments[collName] = &schemapb.LongArray{Data: onFlushSegmentIDs}
flushColl2Segments[collName] = &schemapb.LongArray{Data: resp.GetFlushSegmentIDs()}
coll2SealTimes[collName] = timeOfSeal.Unix()
if hasTextFields {
coll2FlushTs[collName] = 0
} else {
coll2FlushTs[collName] = flushTs
}
coll2FlushTs[collName] = flushTs
channelCps = resp.GetChannelCps()
}
t.result = &milvuspb.FlushResponse{
+3
View File
@@ -105,6 +105,9 @@ func (it *importTask) PreExecute(ctx context.Context) error {
if schema.CollectionSchema == nil || len(schema.GetFields()) == 0 {
return merr.WrapErrImportFailed("collection schema has no fields")
}
if err := validateTextStorageV3Enabled(schema.CollectionSchema); err != nil {
return err
}
it.schema = schema
channels, err := node.chMgr.getVChannels(collectionID)
+3
View File
@@ -157,6 +157,9 @@ func (it *insertTask) PreExecute(ctx context.Context) error {
}
it.schema = schema.CollectionSchema
it.schemaVersion = schema.Version
if err := validateTextStorageV3Enabled(it.schema); err != nil {
return err
}
if err := genFunctionFields(ctx, it.insertMsg, schema, false); err != nil {
return err
+40
View File
@@ -77,6 +77,46 @@ func TestRepackInsertDataForStreamingServicePreservesExplicitZeroSchemaVersion(t
assert.Equal(t, int32(0), header.GetSchemaVersion())
}
func TestInsertTaskPreExecuteTextRequiresStorageV3(t *testing.T) {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
t.Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
})
oldCache := globalMetaCache
t.Cleanup(func() {
globalMetaCache = oldCache
})
const (
dbName = "db"
collectionName = "text_collection"
)
schema := newSchemaInfo(newTextSchemaForStorageV3Test(collectionName))
cache := NewMockCache(t)
cache.EXPECT().GetCollectionID(mock.Anything, dbName, collectionName).Return(int64(100), nil)
cache.EXPECT().GetCollectionInfo(mock.Anything, dbName, collectionName, int64(100)).Return(&collectionInfo{}, nil)
cache.EXPECT().GetCollectionSchema(mock.Anything, dbName, collectionName).Return(schema, nil)
globalMetaCache = cache
task := &insertTask{
ctx: context.Background(),
insertMsg: &BaseInsertTask{
InsertRequest: &msgpb.InsertRequest{
Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_Insert},
DbName: dbName,
CollectionName: collectionName,
NumRows: 1,
},
},
}
err := task.PreExecute(context.Background())
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "TEXT field requires StorageV3")
}
func TestInsertTask_CheckAligned(t *testing.T) {
var err error
+3
View File
@@ -694,6 +694,9 @@ func (t *queryTask) PreExecute(ctx context.Context) error {
return err
}
t.schema = schema
if err := validateTextStorageV3Enabled(t.schema.CollectionSchema); err != nil {
return err
}
err = common.CheckNamespace(t.schema.CollectionSchema, t.request.Namespace)
if err != nil {
return err
+3
View File
@@ -178,6 +178,9 @@ func (t *searchTask) PreExecute(ctx context.Context) error {
log.Warn("get collection schema failed", zap.Error(err))
return err
}
if err := validateTextStorageV3Enabled(t.schema.CollectionSchema); err != nil {
return err
}
collectionInfo, err2 := globalMetaCache.GetCollectionInfo(ctx, t.request.GetDbName(), collectionName, t.CollectionID)
if err2 != nil {
+38
View File
@@ -81,6 +81,44 @@ func TestSearchTaskFillResultSkipsTopksInsufficientForSearchAggregation(t *testi
require.False(t, task.resultSizeInsufficient)
}
func TestSearchTaskPreExecuteTextRequiresStorageV3(t *testing.T) {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
t.Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
})
oldCache := globalMetaCache
t.Cleanup(func() {
globalMetaCache = oldCache
})
const (
dbName = "db"
collectionName = "text_collection"
collectionID = int64(100)
)
schema := newSchemaInfo(newTextSchemaForStorageV3Test(collectionName))
cache := NewMockCache(t)
cache.EXPECT().GetCollectionID(mock.Anything, dbName, collectionName).Return(collectionID, nil)
cache.EXPECT().GetCollectionSchema(mock.Anything, dbName, collectionName).Return(schema, nil)
globalMetaCache = cache
task := &searchTask{
ctx: context.Background(),
SearchRequest: &internalpb.SearchRequest{},
request: &milvuspb.SearchRequest{
DbName: dbName,
CollectionName: collectionName,
},
}
require.NoError(t, task.OnEnqueue())
err := task.PreExecute(context.Background())
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "TEXT field requires StorageV3")
}
func TestSearchTask_PostExecute(t *testing.T) {
var err error
+87
View File
@@ -240,6 +240,24 @@ func constructCollectionSchemaByDataType(collectionName string, fieldName2DataTy
}
}
func newTextSchemaForStorageV3Test(collectionName string) *schemapb.CollectionSchema {
return &schemapb.CollectionSchema{
Name: collectionName,
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: testInt64Field, DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_Text},
{
FieldID: 102,
Name: testFloatVecField,
DataType: schemapb.DataType_FloatVector,
TypeParams: []*commonpb.KeyValuePair{
{Key: common.DimKey, Value: strconv.Itoa(testVecDim)},
},
},
},
}
}
func constructCollectionSchemaWithTTLField(collectionName string, ttlField string) *schemapb.CollectionSchema {
pk := &schemapb.FieldSchema{
FieldID: 100,
@@ -3980,6 +3998,43 @@ func Test_loadCollectionTask_Execute(t *testing.T) {
})
}
func TestLoadCollectionTaskExecuteTextRequiresStorageV3(t *testing.T) {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
t.Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
})
oldCache := globalMetaCache
t.Cleanup(func() {
globalMetaCache = oldCache
})
const (
dbName = "db"
collectionName = "text_collection"
collectionID = int64(100)
)
schema := newSchemaInfo(newTextSchemaForStorageV3Test(collectionName))
cache := NewMockCache(t)
cache.EXPECT().GetCollectionID(mock.Anything, dbName, collectionName).Return(collectionID, nil)
cache.EXPECT().GetCollectionSchema(mock.Anything, dbName, collectionName).Return(schema, nil)
globalMetaCache = cache
task := &loadCollectionTask{
LoadCollectionRequest: &milvuspb.LoadCollectionRequest{
Base: commonpbutil.NewMsgBase(),
DbName: dbName,
CollectionName: collectionName,
},
ctx: context.Background(),
}
err := task.Execute(context.Background())
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "TEXT field requires StorageV3")
}
func Test_loadPartitionTask_Execute(t *testing.T) {
qc := NewMixCoordMock()
@@ -7003,6 +7058,38 @@ func TestValidateAddFieldRequest(t *testing.T) {
assert.NoError(t, err)
})
t.Run("text field requires storage v3", func(t *testing.T) {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
t.Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
})
schema := baseSchema()
newField := &schemapb.FieldSchema{
Name: "text_field",
DataType: schemapb.DataType_Text,
Nullable: true,
}
err := validateAddFieldRequest(schema, newField)
assert.Error(t, err)
assert.ErrorIs(t, err, merr.ErrParameterInvalid)
assert.Contains(t, err.Error(), "TEXT field requires StorageV3")
})
t.Run("text field allowed with storage v3", func(t *testing.T) {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
t.Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
})
schema := baseSchema()
newField := &schemapb.FieldSchema{
Name: "text_field",
DataType: schemapb.DataType_Text,
Nullable: true,
}
err := validateAddFieldRequest(schema, newField)
assert.NoError(t, err)
})
t.Run("field count exceeds MaxFieldNum", func(t *testing.T) {
schema := baseSchema()
// Build exactly MaxFieldNum fields so that the check triggers.
+3
View File
@@ -1290,6 +1290,9 @@ func (it *upsertTask) PreExecute(ctx context.Context) error {
}
it.schema = schema
it.schemaVersion = schema.Version
if err := validateTextStorageV3Enabled(schema.CollectionSchema); err != nil {
return err
}
// Validate any FieldPartialUpdateOp directives attached to FieldData.
// A non-REPLACE op implicitly promotes the request to partial_update=true
+2 -1
View File
@@ -2102,7 +2102,8 @@ func (suite *TaskSuite) TestExecutor_MoveSegmentTask() {
suite.broker,
suite.target,
suite.cluster,
suite.nodeMgr)
suite.nodeMgr,
)
// Verify shard leader ID was set for load action in move task
executor.executeSegmentAction(moveTask, 0)
+27 -40
View File
@@ -37,6 +37,7 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/distributed/streaming"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/querynodev2/cluster"
"github.com/milvus-io/milvus/internal/querynodev2/delegator/deletebuffer"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
@@ -86,7 +87,6 @@ type ShardDelegator interface {
// data
ProcessInsert(insertRecords map[int64]*InsertData)
ProcessDelete(deleteData []*DeleteData, ts uint64)
ProcessManualFlush(ctx context.Context, flushTs uint64) error
LoadGrowing(ctx context.Context, infos []*querypb.SegmentLoadInfo, version int64) error
LoadL0(ctx context.Context, infos []*querypb.SegmentLoadInfo, version int64) error
LoadSegments(ctx context.Context, req *querypb.LoadSegmentsRequest) error
@@ -183,11 +183,13 @@ type shardDelegator struct {
// for slow down the delegator consumption and reduce the timetick dispatch frequency.
latestRequiredMVCCTimeTick *atomic.Uint64
// growing segment flush support for TEXT collections
// checkpointTracker tracks offset -> MsgPosition mapping for Growing Segments
// checkpointTracker is still updated by legacy growing ingest hooks. It no
// longer drives QueryNode-side metadata commit; WAL flusher owns commit.
checkpointTracker *segments.CheckpointTracker
// growingFlushManager manages periodic flush of Growing Segments (for TEXT collections)
growingFlushManager *segments.GrowingFlushManager
// growingSourceRegistration is the process-local registry lease for this
// delegator's optional growing-source source.
growingSourceRegistration *syncmgr.GrowingSourceRegistration
growingSourceProvider *delegatorGrowingSourceProvider
}
// getLogger returns the zap logger with pre-defined shard attributes.
@@ -268,9 +270,6 @@ func (sd *shardDelegator) prepareSearchFunction(req *internalpb.SearchRequest) (
// Start sets delegator to working state.
func (sd *shardDelegator) Start() {
sd.lifetime.SetState(lifetime.Working)
if sd.growingFlushManager != nil {
sd.growingFlushManager.Start(context.Background())
}
}
// Collection returns delegator collection id.
@@ -1327,17 +1326,16 @@ func (sd *shardDelegator) Close() {
sd.tsCond.L.Unlock()
sd.lifetime.Wait()
if sd.growingSourceProvider != nil {
sd.growingSourceProvider.Deactivate()
}
// Stop background snapshot loop before refunding candidates
sd.distribution.Close()
// Refund all sealed segment candidates in distribution
sd.distribution.RefundAllCandidates()
// stop growing flush manager
if sd.growingFlushManager != nil {
sd.growingFlushManager.Stop()
}
// clean idf oracle
if idfOracle := sd.getIDFOracle(); idfOracle != nil {
idfOracle.Close()
@@ -1481,25 +1479,17 @@ func NewShardDelegator(ctx context.Context, collectionID UniqueID, replicaID Uni
sd.publishIDFOracle(idfOracle)
}
// initialize GrowingFlushManager for TEXT collections
// this enables incremental flush of Growing Segments to preserve TEXT data
if sd.hasTextFields() {
// Register growing-source segments as optional local flush sources. Metadata
// commit is still owned by WAL flusher / WriteBuffer.
if sd.useGrowingSourceFlush() {
sd.checkpointTracker = segments.NewCheckpointTracker()
if binlogSaver != nil {
sd.growingFlushManager = segments.NewGrowingFlushManager(
collectionID,
channel,
collection.Schema(),
manager.Collection,
manager.Segment,
binlogSaver,
chunkManager,
sd.checkpointTracker,
)
log.Info("initialized GrowingFlushManager for TEXT collection")
} else {
log.Warn("binlogSaver is nil, GrowingFlushManager not initialized for TEXT collection")
}
sd.growingSourceProvider = newDelegatorGrowingSourceProvider(manager.Segment, func(ctx context.Context, fenceTs uint64) error {
_, err := sd.waitTSafe(ctx, fenceTs)
return err
})
sd.growingSourceRegistration = syncmgr.DefaultGrowingSourceRegistry().Register(sd.vchannelName, sd.growingSourceProvider)
sd.growingSourceProvider.SetRegistration(sd.growingSourceRegistration)
log.Info("registered growing-source source support")
}
sd.tsCond = syncutil.NewContextCond(&sync.Mutex{})
@@ -1508,17 +1498,14 @@ func NewShardDelegator(ctx context.Context, collectionID UniqueID, replicaID Uni
return sd, nil
}
// hasTextFields returns true if the collection has any TEXT type fields.
func (sd *shardDelegator) hasTextFields() bool {
if sd.collection == nil {
// useGrowingSourceFlush returns true when the collection should expose growing segments as a flush source.
func (sd *shardDelegator) useGrowingSourceFlush() bool {
if sd == nil || sd.collection == nil {
return false
}
for _, field := range sd.collection.Schema().GetFields() {
if field.GetDataType() == schemapb.DataType_Text {
return true
}
}
return false
return typeutil.UseGrowingSourceFlush(sd.collection.Schema(),
paramtable.Get().CommonCfg.UseLoonFFI.GetAsBool(),
paramtable.Get().CommonCfg.EnableGrowingSourceFlush.GetAsBool())
}
func (sd *shardDelegator) RunAnalyzer(ctx context.Context, req *querypb.RunAnalyzerRequest) ([]*milvuspb.AnalyzerResult, error) {
+18 -109
View File
@@ -148,9 +148,9 @@ func (sd *shardDelegator) ProcessInsert(insertRecords map[int64]*InsertData) {
}
growing.UpdatePkCandidate(insertData.PrimaryKeys)
// record batch info for checkpoint tracking (TEXT collections only)
// record batch info for checkpoint tracking (growing-source collections only)
if sd.checkpointTracker != nil {
endOffset := growing.RowNum()
endOffset := growing.InsertCount()
sd.checkpointTracker.RecordBatch(
growing.ID(),
endOffset,
@@ -248,73 +248,6 @@ func (sd *shardDelegator) ProcessDelete(deleteData []*DeleteData, ts uint64) {
Observe(float64(tr.ElapseSpan().Milliseconds()))
}
// ProcessManualFlush handles manual flush request for TEXT collections.
// It triggers immediate flush of all unflushed data in Growing Segments.
// This is called when user explicitly requests flush via collection.flush().
func (sd *shardDelegator) ProcessManualFlush(ctx context.Context, flushTs uint64) error {
// Only process for TEXT collections that have GrowingFlushManager
if sd.growingFlushManager == nil || sd.checkpointTracker == nil {
return nil
}
log := sd.getLogger(ctx).With(zap.Uint64("flushTs", flushTs))
log.Info("processing manual flush for TEXT collection")
// Get all tracked segment IDs
segmentIDs := sd.checkpointTracker.GetSegmentIDs()
if len(segmentIDs) == 0 {
log.Debug("no segments to flush")
return nil
}
// Submit to the dynamic CGO pool to reuse the same concurrency control
// as other CGO operations (pool size = CPU cores).
pool := segments.GetDynamicPool()
futures := make([]*conc.Future[any], 0, len(segmentIDs))
for _, segID := range segmentIDs {
segment := sd.segmentManager.GetGrowing(segID)
if segment == nil {
continue
}
id := segID
future := pool.Submit(func() (any, error) {
if err := sd.growingFlushManager.ForceSyncAndSeal(ctx, id); err != nil {
log.Warn("failed to ForceSyncAndSeal during manual flush",
zap.Int64("segmentID", id),
zap.Error(err))
return nil, err
}
log.Info("ForceSyncAndSeal completed during manual flush",
zap.Int64("segmentID", id))
return nil, nil
})
futures = append(futures, future)
}
// wait for all flush tasks to complete and collect errors
conc.AwaitAll(futures...)
var firstErr error
failCount := 0
for _, f := range futures {
if _, err := f.Await(); err != nil {
failCount++
if firstErr == nil {
firstErr = err
}
}
}
if failCount > 0 {
log.Warn("manual flush completed with errors",
zap.Int("segmentCount", len(segmentIDs)),
zap.Int("failCount", failCount),
zap.Error(firstErr))
return fmt.Errorf("manual flush failed for %d/%d segments: %w", failCount, len(segmentIDs), firstErr)
}
log.Info("manual flush completed", zap.Int("segmentCount", len(segmentIDs)))
return nil
}
type BatchApplyRet = struct {
DeleteDataIdx int
StartIdx int
@@ -480,14 +413,18 @@ func (sd *shardDelegator) LoadGrowing(ctx context.Context, infos []*querypb.Segm
segmentIDs = lo.Map(loaded, func(segment segments.Segment, _ int) int64 { return segment.ID() })
log.Info("load growing segments done", zap.Int64s("segmentIDs", segmentIDs))
// initialize checkpoint tracking for recovered growing segments (TEXT collections only)
// initialize checkpoint tracking for recovered growing segments (growing-source collections only)
if sd.checkpointTracker != nil {
for _, segment := range loaded {
// the segment was recovered from binlog, so the current row count is the flushed offset
flushedOffset := segment.RowNum()
manifest := segment.LoadInfo().GetManifestPath()
if manifest == "" && flushedOffset > 0 {
return fmt.Errorf("recovered growing segment %d has %d flushed rows but no manifest path, cannot safely resume flush", segment.ID(), flushedOffset)
log.Info("initialize checkpoint tracker for legacy recovered growing segment",
zap.Int64("segmentID", segment.ID()),
zap.Int64("flushedOffset", flushedOffset))
sd.checkpointTracker.InitSegment(segment.ID(), flushedOffset)
continue
}
sd.checkpointTracker.InitSegmentWithManifest(segment.ID(), flushedOffset, manifest)
log.Info("initialized checkpoint tracker for recovered growing segment",
@@ -1184,6 +1121,7 @@ func (sd *shardDelegator) ReleaseSegments(ctx context.Context, req *querypb.Rele
case querypb.DataScope_Historical:
sealed = lo.Map(req.GetSegmentIDs(), convertSealed)
}
signal := sd.distribution.RemoveDistributions(sealed, growing)
// wait cleared signal
<-signal
@@ -1206,50 +1144,16 @@ func (sd *shardDelegator) ReleaseSegments(ctx context.Context, req *querypb.Rele
// - Sealed segment candidates (BloomFilterSet) are refunded in RemoveDistributions
// - Growing segment candidates (LocalSegment) are managed by segmentManager.Release()
if len(growing) > 0 {
// For TEXT collections: best-effort flush before releasing growing segments.
// Flush failure does NOT block release because:
// 1. Data is still in WAL — channel checkpoint clamping ensures WAL won't
// be truncated beyond unflushed growing segment data.
// 2. On recovery, WAL replays from the clamped checkpoint, restoring data.
// 3. Blocking release on flush failure would prevent QueryCoord balance
// and QueryNode graceful shutdown.
if sd.growingFlushManager != nil && sd.checkpointTracker != nil {
for _, entry := range growing {
segID := entry.SegmentID
segment := sd.segmentManager.GetGrowing(segID)
if segment == nil {
continue
}
// growing-source data is not flushed directly from QueryNode during
// release. WAL flusher owns metadata commits; if a source-backed sync
// has not committed yet, WriteBuffer checkpoint pin keeps WAL replayable.
flushedOffset := sd.checkpointTracker.GetFlushedOffset(segID)
currentOffset := segment.RowNum()
if flushedOffset >= currentOffset {
continue
}
log := sd.getLogger(ctx).With(zap.Int64("segmentID", segID))
log.Info("best-effort flush before release",
zap.Int64("unflushedRows", currentOffset-flushedOffset))
if err := sd.growingFlushManager.ForceSync(ctx, segID); err != nil {
log.Warn("best-effort flush failed before release, data will be recovered from WAL",
zap.Error(err))
}
}
}
// clean up checkpoint tracking for released growing segments (TEXT collections only)
// clean up checkpoint tracking for released growing segments (growing-source collections only)
if sd.checkpointTracker != nil {
for _, entry := range growing {
sd.checkpointTracker.RemoveSegment(entry.SegmentID)
}
}
// clean up sealed segment tracking to prevent memory leak (TEXT collections only)
if sd.growingFlushManager != nil {
for _, entry := range growing {
sd.growingFlushManager.RemoveSealedSegment(entry.SegmentID)
}
}
}
var releaseErr error
@@ -1273,6 +1177,11 @@ func (sd *shardDelegator) ReleaseSegments(ctx context.Context, req *querypb.Rele
if releaseErr != nil {
return releaseErr
}
if len(growing) > 0 && sd.growingSourceProvider != nil {
for _, entry := range growing {
sd.growingSourceProvider.ClearReleasePrepared(entry.SegmentID)
}
}
return nil
}
@@ -38,6 +38,7 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/mocks"
"github.com/milvus-io/milvus/internal/mocks/util/mock_segcore"
"github.com/milvus-io/milvus/internal/querynodev2/cluster"
@@ -190,6 +191,40 @@ func (s *DelegatorDataSuite) genNormalCollection() {
})
}
func (s *DelegatorDataSuite) genTextCollection() {
s.manager.Collection.PutOrRef(s.collectionID, &schemapb.CollectionSchema{
Name: "TestTextCollection",
Fields: []*schemapb.FieldSchema{
{
Name: "id",
FieldID: 100,
IsPrimaryKey: true,
DataType: schemapb.DataType_Int64,
},
{
Name: "text",
FieldID: 101,
DataType: schemapb.DataType_Text,
},
{
Name: "vector",
FieldID: 102,
DataType: schemapb.DataType_FloatVector,
TypeParams: []*commonpb.KeyValuePair{
{
Key: common.DimKey,
Value: "128",
},
},
},
},
}, nil, &querypb.LoadMetaInfo{
LoadType: querypb.LoadType_LoadCollection,
PartitionIDs: []int64{1001},
SchemaVersion: tsoutil.ComposeTSByTime(time.Now(), 0),
})
}
func (s *DelegatorDataSuite) genCollectionWithFunction() {
s.manager.Collection.PutOrRef(s.collectionID, &schemapb.CollectionSchema{
Name: "TestCollection",
@@ -230,6 +265,11 @@ func (s *DelegatorDataSuite) genCollectionWithFunction() {
}
func (s *DelegatorDataSuite) SetupTest() {
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
s.T().Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
})
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
@@ -249,6 +289,15 @@ func (s *DelegatorDataSuite) SetupTest() {
s.delegator = sd
}
func (s *DelegatorDataSuite) enableGrowingSourceFlush() {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "true")
s.T().Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
})
}
func (s *DelegatorDataSuite) TestProcessInsert() {
s.Run("normal_insert", func() {
s.delegator.ProcessInsert(map[int64]*InsertData{
@@ -342,6 +391,7 @@ func (s *DelegatorDataSuite) TestProcessDelete() {
ms.EXPECT().Partition().Return(info.GetPartitionID())
ms.EXPECT().Indexes().Return(nil)
ms.EXPECT().RowNum().Return(info.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(info)
ms.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
ms.EXPECT().MayPkExist(mock.Anything).RunAndReturn(func(lc *storage.LocationsCache) bool {
return lc.GetPk().EQ(storage.NewInt64PrimaryKey(10))
@@ -581,12 +631,51 @@ func (s *DelegatorDataSuite) TestLoadGrowingWithBM25() {
mockSegment.EXPECT().ID().Return(int64(111))
// Note: Type() is no longer called since pkOracle was removed
// Candidate is now stored directly in SegmentEntry
mockSegment.EXPECT().RowNum().Return(int64(0)).Maybe()
mockSegment.EXPECT().LoadInfo().Return(&querypb.SegmentLoadInfo{SegmentID: 111}).Maybe()
mockSegment.EXPECT().GetBM25Stats().Return(map[int64]*storage.BM25Stats{})
err := s.delegator.LoadGrowing(context.Background(), []*querypb.SegmentLoadInfo{{SegmentID: 1}}, 1)
s.NoError(err)
}
func (s *DelegatorDataSuite) TestLoadLegacyGrowingSourceWithoutManifest() {
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
s.manager = segments.NewManager()
s.genTextCollection()
delegator, err := NewShardDelegator(context.Background(), s.collectionID, s.replicaID, s.vchannelName, s.version, s.workerManager, s.manager, s.loader, 10000, nil, s.chunkManager, NewChannelQueryView(nil, nil, nil, initialTargetVersion), nil)
s.Require().NoError(err)
sd, ok := delegator.(*shardDelegator)
s.Require().True(ok)
s.Require().NotNil(sd.checkpointTracker)
s.delegator = sd
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 1001,
CollectionID: s.collectionID,
PartitionID: 500,
InsertChannel: s.vchannelName,
NumOfRows: 50,
}
s.loader.EXPECT().
Load(mock.Anything, s.collectionID, segments.SegmentTypeGrowing, int64(0), mock.Anything).
Call.Return(func(ctx context.Context, collectionID int64, segmentType segments.SegmentType, version int64, infos ...*querypb.SegmentLoadInfo) []segments.Segment {
ms := segments.NewMockSegment(s.T())
ms.EXPECT().ID().Return(loadInfo.GetSegmentID())
ms.EXPECT().Partition().Return(loadInfo.GetPartitionID())
ms.EXPECT().RowNum().Return(loadInfo.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(loadInfo)
return []segments.Segment{ms}
}, nil)
err = s.delegator.LoadGrowing(context.Background(), []*querypb.SegmentLoadInfo{loadInfo}, 0)
s.Require().NoError(err)
s.Equal(int64(50), s.delegator.checkpointTracker.GetFlushedOffset(loadInfo.GetSegmentID()))
s.Empty(s.delegator.checkpointTracker.GetAcknowledgedManifest(loadInfo.GetSegmentID()))
}
func (s *DelegatorDataSuite) TestLoadSegmentsWithBm25() {
s.genCollectionWithFunction()
s.Run("normal_run", func() {
@@ -1483,6 +1572,7 @@ func (s *DelegatorDataSuite) TestReleaseSegment() {
ms.EXPECT().Collection().Return(info.GetCollectionID())
ms.EXPECT().Indexes().Return(nil)
ms.EXPECT().RowNum().Return(info.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(info)
ms.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
ms.EXPECT().MayPkExist(mock.Anything).Call.Return(func(pk storage.PrimaryKey) bool {
return pk.EQ(storage.NewInt64PrimaryKey(10))
@@ -1619,6 +1709,289 @@ func (s *DelegatorDataSuite) TestReleaseSegment() {
s.NoError(err)
}
func (s *DelegatorDataSuite) TestReleaseGrowingSourceAfterPreparedHandoff() {
ctx := context.Background()
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
s.genTextCollection()
s.enableGrowingSourceFlush()
delegator, err := NewShardDelegator(
ctx,
s.collectionID,
s.replicaID,
s.vchannelName,
s.version,
s.workerManager,
s.manager,
s.loader,
10000,
nil,
s.chunkManager,
NewChannelQueryView(nil, nil, nil, initialTargetVersion),
nil)
s.Require().NoError(err)
sd := delegator.(*shardDelegator)
defer sd.Close()
const (
segmentID = int64(1001)
partitionID = int64(500)
targetOffset = int64(10)
)
segment := segments.NewMockSegment(s.T())
segment.EXPECT().ID().Return(segmentID).Maybe()
segment.EXPECT().Version().Return(int64(1)).Maybe()
segment.EXPECT().Shard().Return(s.channel).Maybe()
segment.EXPECT().Collection().Return(s.collectionID).Maybe()
segment.EXPECT().Partition().Return(partitionID).Maybe()
segment.EXPECT().Type().Return(segments.SegmentTypeGrowing).Maybe()
segment.EXPECT().Level().Return(datapb.SegmentLevel_Legacy).Maybe()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(targetOffset).Once()
segment.EXPECT().Unpin().Maybe()
s.manager.Segment.Put(ctx, segments.SegmentTypeGrowing, segment)
sd.distribution.AddGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: partitionID,
Version: 1,
})
worker := &cluster.MockWorker{}
worker.EXPECT().ReleaseSegments(mock.Anything, mock.AnythingOfType("*querypb.ReleaseSegmentsRequest")).Return(nil).Once()
s.workerManager.EXPECT().GetWorker(mock.Anything, paramtable.GetNodeID()).Return(worker, nil).Once()
err = sd.growingSourceProvider.PrepareGrowingSourceReleaseHandoff(ctx, 10000, []syncmgr.GrowingSourceReleaseHandoffSegment{
{
SegmentID: segmentID,
TargetOffset: targetOffset,
},
})
s.Require().NoError(err)
err = sd.ReleaseSegments(ctx, &querypb.ReleaseSegmentsRequest{
Base: commonpbutil.NewMsgBase(),
NodeID: paramtable.GetNodeID(),
CollectionID: s.collectionID,
SegmentIDs: []int64{segmentID},
Scope: querypb.DataScope_Streaming,
Shard: s.vchannelName,
}, false)
s.Require().NoError(err)
}
func (s *DelegatorDataSuite) TestReleaseGrowingSourceAfterFencePreparedHandoff() {
ctx := context.Background()
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
s.genTextCollection()
s.enableGrowingSourceFlush()
delegator, err := NewShardDelegator(
ctx,
s.collectionID,
s.replicaID,
s.vchannelName,
s.version,
s.workerManager,
s.manager,
s.loader,
10000,
nil,
s.chunkManager,
NewChannelQueryView(nil, nil, nil, initialTargetVersion),
nil)
s.Require().NoError(err)
sd := delegator.(*shardDelegator)
defer sd.Close()
const segmentID = int64(1001)
sd.distribution.AddGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: 500,
Version: 1,
})
err = sd.growingSourceProvider.PrepareGrowingSourceReleaseHandoff(ctx, 10000, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: segmentID},
})
s.Require().NoError(err)
worker := &cluster.MockWorker{}
worker.EXPECT().ReleaseSegments(mock.Anything, mock.AnythingOfType("*querypb.ReleaseSegmentsRequest")).Return(nil).Once()
s.workerManager.EXPECT().GetWorker(mock.Anything, paramtable.GetNodeID()).Return(worker, nil).Once()
err = sd.ReleaseSegments(ctx, &querypb.ReleaseSegmentsRequest{
Base: commonpbutil.NewMsgBase(),
NodeID: paramtable.GetNodeID(),
CollectionID: s.collectionID,
SegmentIDs: []int64{segmentID},
Scope: querypb.DataScope_Streaming,
Shard: s.vchannelName,
Checkpoint: &msgpb.MsgPosition{Timestamp: 10000},
}, false)
s.Require().NoError(err)
}
func (s *DelegatorDataSuite) TestReleaseGrowingSourceAfterNoRetainPreparedHandoff() {
ctx := context.Background()
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
s.genTextCollection()
s.enableGrowingSourceFlush()
delegator, err := NewShardDelegator(
ctx,
s.collectionID,
s.replicaID,
s.vchannelName,
s.version,
s.workerManager,
s.manager,
s.loader,
10000,
nil,
s.chunkManager,
NewChannelQueryView(nil, nil, nil, initialTargetVersion),
nil)
s.Require().NoError(err)
sd := delegator.(*shardDelegator)
defer sd.Close()
const segmentID = int64(1001)
sd.distribution.AddGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: 500,
Version: 1,
})
err = sd.growingSourceProvider.PrepareGrowingSourceReleaseHandoff(ctx, 10000, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: segmentID},
})
s.Require().NoError(err)
worker := &cluster.MockWorker{}
worker.EXPECT().ReleaseSegments(mock.Anything, mock.AnythingOfType("*querypb.ReleaseSegmentsRequest")).Return(nil).Once()
s.workerManager.EXPECT().GetWorker(mock.Anything, paramtable.GetNodeID()).Return(worker, nil).Once()
err = sd.ReleaseSegments(ctx, &querypb.ReleaseSegmentsRequest{
Base: commonpbutil.NewMsgBase(),
NodeID: paramtable.GetNodeID(),
CollectionID: s.collectionID,
SegmentIDs: []int64{segmentID},
Scope: querypb.DataScope_Streaming,
Shard: s.vchannelName,
Checkpoint: &msgpb.MsgPosition{Timestamp: 10000},
}, false)
s.Require().NoError(err)
}
func (s *DelegatorDataSuite) TestReleaseGrowingSourceWithoutPreparedHandoff() {
ctx := context.Background()
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
s.genTextCollection()
s.enableGrowingSourceFlush()
delegator, err := NewShardDelegator(
ctx,
s.collectionID,
s.replicaID,
s.vchannelName,
s.version,
s.workerManager,
s.manager,
s.loader,
10000,
nil,
s.chunkManager,
NewChannelQueryView(nil, nil, nil, initialTargetVersion),
nil)
s.Require().NoError(err)
sd := delegator.(*shardDelegator)
defer sd.Close()
const segmentID = int64(1001)
sd.distribution.AddGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: 500,
Version: 1,
})
worker := &cluster.MockWorker{}
worker.EXPECT().ReleaseSegments(mock.Anything, mock.AnythingOfType("*querypb.ReleaseSegmentsRequest")).Return(nil).Once()
s.workerManager.EXPECT().GetWorker(mock.Anything, paramtable.GetNodeID()).Return(worker, nil).Once()
err = sd.ReleaseSegments(ctx, &querypb.ReleaseSegmentsRequest{
Base: commonpbutil.NewMsgBase(),
NodeID: paramtable.GetNodeID(),
CollectionID: s.collectionID,
SegmentIDs: []int64{segmentID},
Scope: querypb.DataScope_Streaming,
Shard: s.vchannelName,
Checkpoint: &msgpb.MsgPosition{Timestamp: 10000},
}, false)
s.Require().NoError(err)
}
func (s *DelegatorDataSuite) TestReleaseGrowingSourceDroppedChannelWithoutPreparedHandoff() {
ctx := context.Background()
s.workerManager = &cluster.MockManager{}
s.manager = segments.NewManager()
s.loader = &segments.MockLoader{}
s.genTextCollection()
s.enableGrowingSourceFlush()
delegator, err := NewShardDelegator(
ctx,
s.collectionID,
s.replicaID,
s.vchannelName,
s.version,
s.workerManager,
s.manager,
s.loader,
10000,
nil,
s.chunkManager,
NewChannelQueryView(nil, nil, nil, initialTargetVersion),
nil)
s.Require().NoError(err)
sd := delegator.(*shardDelegator)
defer sd.Close()
const segmentID = int64(1001)
sd.distribution.AddGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: 500,
Version: 1,
})
worker := &cluster.MockWorker{}
worker.EXPECT().ReleaseSegments(mock.Anything, mock.AnythingOfType("*querypb.ReleaseSegmentsRequest")).Return(nil).Once()
s.workerManager.EXPECT().GetWorker(mock.Anything, paramtable.GetNodeID()).Return(worker, nil).Once()
err = sd.ReleaseSegments(ctx, &querypb.ReleaseSegmentsRequest{
Base: commonpbutil.NewMsgBase(),
NodeID: paramtable.GetNodeID(),
CollectionID: s.collectionID,
SegmentIDs: []int64{segmentID},
Scope: querypb.DataScope_Streaming,
Shard: s.vchannelName,
Checkpoint: &msgpb.MsgPosition{Timestamp: typeutil.MaxTimestamp},
}, false)
s.Require().NoError(err)
}
func (s *DelegatorDataSuite) TestLoadPartitionStats() {
segStats := make(map[UniqueID]storage.SegmentStats)
centroid := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
@@ -1933,18 +2306,6 @@ func (s *DelegatorDataSuite) TestDelegatorData_ExcludeSegments() {
s.True(s.delegator.VerifyExcludedSegments(1, 5))
}
func (s *DelegatorDataSuite) TestProcessManualFlush_NilManager() {
// When growingFlushManager is nil (non-TEXT collection), should return immediately without panic
s.Nil(s.delegator.growingFlushManager)
s.delegator.ProcessManualFlush(context.Background(), 1000)
}
func (s *DelegatorDataSuite) TestProcessManualFlush_NilTracker() {
// When checkpointTracker is nil, should return immediately without panic
s.delegator.checkpointTracker = nil
s.delegator.ProcessManualFlush(context.Background(), 1000)
}
func TestSegmentEffectiveTs(t *testing.T) {
// Import segment: commit_timestamp takes precedence over start_position.Timestamp
info := &querypb.SegmentLoadInfo{
@@ -91,6 +91,11 @@ func (s *DelegatorSuite) TearDownSuite() {
}
func (s *DelegatorSuite) SetupTest() {
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
s.T().Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
})
s.collectionID = 1000
s.partitionIDs = []int64{500, 501}
s.replicaID = 65535
@@ -110,6 +115,7 @@ func (s *DelegatorSuite) SetupTest() {
ms.EXPECT().Collection().Return(info.GetCollectionID())
ms.EXPECT().Indexes().Return(nil)
ms.EXPECT().RowNum().Return(info.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(info)
ms.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
return ms
})
@@ -67,6 +67,11 @@ func (s *StreamingForwardSuite) SetupSuite() {
}
func (s *StreamingForwardSuite) SetupTest() {
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
s.T().Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
})
s.collectionID = 1000
s.partitionIDs = []int64{500, 501}
s.replicaID = 65535
@@ -86,6 +91,7 @@ func (s *StreamingForwardSuite) SetupTest() {
ms.EXPECT().Collection().Return(info.GetCollectionID())
ms.EXPECT().Indexes().Return(nil)
ms.EXPECT().RowNum().Return(info.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(info)
ms.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
return ms
})
@@ -300,6 +306,11 @@ func (s *GrowingMergeL0Suite) SetupSuite() {
}
func (s *GrowingMergeL0Suite) SetupTest() {
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
s.T().Cleanup(func() {
paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
})
s.collectionID = 1000
s.partitionIDs = []int64{500, 501}
s.replicaID = 65535
@@ -319,6 +330,7 @@ func (s *GrowingMergeL0Suite) SetupTest() {
ms.EXPECT().Collection().Return(info.GetCollectionID())
ms.EXPECT().Indexes().Return(nil)
ms.EXPECT().RowNum().Return(info.GetNumOfRows())
ms.EXPECT().LoadInfo().Return(info)
ms.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
return ms
})
@@ -0,0 +1,562 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package delegator
import (
"context"
"sync"
"github.com/cockroachdb/errors"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
)
var errGrowingSourceProviderClosed = errors.New("growing source provider is closed")
type delegatorGrowingSourceProvider struct {
segmentManager segments.SegmentManager
waitFence func(context.Context, uint64) error
mu sync.Mutex
cond *sync.Cond
closing bool
deactivated bool
registration *syncmgr.GrowingSourceRegistration
active int
retained map[int64]*retainedGrowingFlushSource
releaseAllowed map[int64]uint64
releasePrepared map[int64]int64
handoffOnly bool
handoffAllowed map[int64]struct{}
}
func newDelegatorGrowingSourceProvider(segmentManager segments.SegmentManager, waitFence func(context.Context, uint64) error) *delegatorGrowingSourceProvider {
provider := &delegatorGrowingSourceProvider{
segmentManager: segmentManager,
waitFence: waitFence,
retained: make(map[int64]*retainedGrowingFlushSource),
releaseAllowed: make(map[int64]uint64),
releasePrepared: make(map[int64]int64),
handoffAllowed: make(map[int64]struct{}),
}
provider.cond = sync.NewCond(&provider.mu)
return provider
}
func (p *delegatorGrowingSourceProvider) SetRegistration(registration *syncmgr.GrowingSourceRegistration) {
p.mu.Lock()
defer p.mu.Unlock()
p.registration = registration
}
func (p *delegatorGrowingSourceProvider) GetGrowingFlushSource(segmentID int64, targetOffset int64, _ *msgpb.MsgPosition) (syncmgr.GrowingFlushSource, syncmgr.GrowingSourceState) {
if !p.acquireLease(segmentID) {
return nil, syncmgr.GrowingSourceUnavailable
}
segment := p.segmentManager.GetGrowing(segmentID)
retained := false
if segment == nil {
var ok bool
segment, ok = p.getRetained(segmentID)
if !ok {
p.releaseLease()
return nil, syncmgr.GrowingSourceUnavailable
}
retained = true
} else if p.isDeactivated() {
p.releaseLease()
return nil, syncmgr.GrowingSourceUnavailable
}
if err := segment.PinIfNotReleased(); err != nil {
p.releaseLease()
return nil, syncmgr.GrowingSourceUnavailable
}
source := &delegatorGrowingFlushSource{segmentID: segmentID, segment: segment, provider: p, targetOffset: targetOffset, retained: retained}
if p.currentOffset(segment) < targetOffset {
return source, syncmgr.GrowingSourcePending
}
return source, syncmgr.GrowingSourceUsable
}
func (p *delegatorGrowingSourceProvider) PrepareGrowingSourceReleaseHandoff(ctx context.Context, fenceTs uint64, segments []syncmgr.GrowingSourceReleaseHandoffSegment) error {
if p.isDeactivated() {
return p.prepareDeactivatedGrowingSourceReleaseHandoff(fenceTs, segments)
}
handoffSnapshot := p.enterHandoffOnly(segments)
if p.waitFence != nil && fenceTs > 0 {
if err := p.waitFence(ctx, fenceTs); err != nil {
p.rollbackHandoffOnly(handoffSnapshot)
return err
}
}
snapshot := p.snapshotRetained(segments)
allowedSegments := make([]syncmgr.GrowingSourceReleaseHandoffSegment, 0, len(segments))
preparedSegments := make([]syncmgr.GrowingSourceReleaseHandoffSegment, 0, len(segments))
for _, segment := range segments {
allowedSegments = append(allowedSegments, segment)
if segment.TargetOffset <= 0 {
continue
}
if err := p.registerRetained(segment.SegmentID, segment.TargetOffset); err != nil {
if errors.Is(err, merr.ErrSegmentNotFound) {
continue
}
p.rollbackRetained(snapshot)
p.rollbackHandoffOnly(handoffSnapshot)
return err
}
preparedSegments = append(preparedSegments, segment)
}
p.markReleaseAllowed(fenceTs, allowedSegments)
p.markReleasePrepared(fenceTs, preparedSegments)
return nil
}
func (p *delegatorGrowingSourceProvider) prepareDeactivatedGrowingSourceReleaseHandoff(fenceTs uint64, segments []syncmgr.GrowingSourceReleaseHandoffSegment) error {
p.mu.Lock()
defer p.mu.Unlock()
for _, segment := range segments {
retained, ok := p.retained[segment.SegmentID]
if !ok {
continue
}
if segment.TargetOffset > retained.targetOffset {
continue
}
if current, ok := p.releaseAllowed[segment.SegmentID]; !ok || current < fenceTs {
p.releaseAllowed[segment.SegmentID] = fenceTs
}
if segment.TargetOffset <= 0 {
continue
}
if current, ok := p.releasePrepared[segment.SegmentID]; !ok || current < segment.TargetOffset {
p.releasePrepared[segment.SegmentID] = segment.TargetOffset
}
}
return nil
}
func (p *delegatorGrowingSourceProvider) registerRetained(segmentID int64, targetOffset int64) error {
p.mu.Lock()
if p.closing {
p.mu.Unlock()
return errGrowingSourceProviderClosed
}
if retained, ok := p.retained[segmentID]; ok {
if retained.targetOffset < targetOffset {
retained.targetOffset = targetOffset
}
p.mu.Unlock()
return nil
}
p.mu.Unlock()
segment := p.segmentManager.GetGrowing(segmentID)
if segment == nil {
return merr.WrapErrSegmentNotFound(segmentID)
}
if err := segment.PinIfNotReleased(); err != nil {
return err
}
currentOffset := p.currentOffset(segment)
if currentOffset < targetOffset {
segment.Unpin()
return errors.Errorf("growing-source segment %d is behind target offset, current=%d target=%d", segmentID, currentOffset, targetOffset)
}
p.mu.Lock()
defer p.mu.Unlock()
if p.closing {
segment.Unpin()
return errGrowingSourceProviderClosed
}
if retained, ok := p.retained[segmentID]; ok {
if retained.targetOffset < targetOffset {
retained.targetOffset = targetOffset
}
segment.Unpin()
return nil
}
p.retained[segmentID] = &retainedGrowingFlushSource{
segment: segment,
targetOffset: targetOffset,
}
return nil
}
type retainedSnapshot struct {
existed bool
source *retainedGrowingFlushSource
targetOffset int64
committedOffset int64
detached bool
}
func (p *delegatorGrowingSourceProvider) snapshotRetained(segments []syncmgr.GrowingSourceReleaseHandoffSegment) map[int64]retainedSnapshot {
p.mu.Lock()
defer p.mu.Unlock()
snapshot := make(map[int64]retainedSnapshot, len(segments))
for _, segment := range segments {
if _, ok := snapshot[segment.SegmentID]; ok {
continue
}
retained, existed := p.retained[segment.SegmentID]
entry := retainedSnapshot{existed: existed, source: retained}
if existed {
entry.targetOffset = retained.targetOffset
entry.committedOffset = retained.committedOffset
entry.detached = retained.detached
}
snapshot[segment.SegmentID] = entry
}
return snapshot
}
func (p *delegatorGrowingSourceProvider) rollbackRetained(snapshot map[int64]retainedSnapshot) {
var toUnpin []segments.Segment
p.mu.Lock()
for segmentID, entry := range snapshot {
current, exists := p.retained[segmentID]
if entry.existed {
p.retained[segmentID] = entry.source
entry.source.targetOffset = entry.targetOffset
entry.source.committedOffset = entry.committedOffset
entry.source.detached = entry.detached
if exists && current != entry.source {
toUnpin = append(toUnpin, current.segment)
}
continue
}
if exists {
delete(p.retained, segmentID)
toUnpin = append(toUnpin, current.segment)
}
}
p.mu.Unlock()
for _, segment := range toUnpin {
segment.Unpin()
}
}
type handoffSnapshot struct {
enabled bool
allowed map[int64]struct{}
}
func (p *delegatorGrowingSourceProvider) enterHandoffOnly(segments []syncmgr.GrowingSourceReleaseHandoffSegment) handoffSnapshot {
p.mu.Lock()
defer p.mu.Unlock()
snapshot := handoffSnapshot{
enabled: p.handoffOnly,
allowed: make(map[int64]struct{}, len(p.handoffAllowed)),
}
for segmentID := range p.handoffAllowed {
snapshot.allowed[segmentID] = struct{}{}
}
p.handoffOnly = true
for _, segment := range segments {
p.handoffAllowed[segment.SegmentID] = struct{}{}
}
return snapshot
}
func (p *delegatorGrowingSourceProvider) rollbackHandoffOnly(snapshot handoffSnapshot) {
p.mu.Lock()
defer p.mu.Unlock()
p.handoffOnly = snapshot.enabled
p.handoffAllowed = snapshot.allowed
}
func (p *delegatorGrowingSourceProvider) markReleaseAllowed(fenceTs uint64, segments []syncmgr.GrowingSourceReleaseHandoffSegment) {
p.mu.Lock()
defer p.mu.Unlock()
for _, segment := range segments {
if current, ok := p.releaseAllowed[segment.SegmentID]; !ok || current < fenceTs {
p.releaseAllowed[segment.SegmentID] = fenceTs
}
}
}
func (p *delegatorGrowingSourceProvider) markReleasePrepared(fenceTs uint64, segments []syncmgr.GrowingSourceReleaseHandoffSegment) {
p.mu.Lock()
defer p.mu.Unlock()
for _, segment := range segments {
if current, ok := p.releaseAllowed[segment.SegmentID]; !ok || current < fenceTs {
p.releaseAllowed[segment.SegmentID] = fenceTs
}
if current, ok := p.releasePrepared[segment.SegmentID]; !ok || current < segment.TargetOffset {
p.releasePrepared[segment.SegmentID] = segment.TargetOffset
}
}
}
func (p *delegatorGrowingSourceProvider) IsReleaseAllowed(segmentID int64, checkpointTs uint64) bool {
p.mu.Lock()
defer p.mu.Unlock()
return p.isReleaseAllowedLocked(segmentID, checkpointTs)
}
func (p *delegatorGrowingSourceProvider) IsReleasePrepared(segmentID int64, checkpointTs uint64) bool {
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := p.releasePrepared[segmentID]; ok {
return p.isReleaseAllowedLocked(segmentID, checkpointTs)
}
return false
}
func (p *delegatorGrowingSourceProvider) isReleaseAllowedLocked(segmentID int64, checkpointTs uint64) bool {
fenceTs, ok := p.releaseAllowed[segmentID]
if !ok {
return false
}
return fenceTs == 0 || checkpointTs == 0 || checkpointTs <= fenceTs
}
func (p *delegatorGrowingSourceProvider) ClearReleasePrepared(segmentID int64) {
p.mu.Lock()
defer p.mu.Unlock()
delete(p.releasePrepared, segmentID)
delete(p.releaseAllowed, segmentID)
delete(p.handoffAllowed, segmentID)
}
func (p *delegatorGrowingSourceProvider) ReleasePreparedSegments() []int64 {
p.mu.Lock()
defer p.mu.Unlock()
segments := make([]int64, 0, len(p.releasePrepared))
for segmentID := range p.releasePrepared {
segments = append(segments, segmentID)
}
return segments
}
func (p *delegatorGrowingSourceProvider) MarkReleaseDetached(segmentID int64) {
p.mu.Lock()
retained, ok := p.retained[segmentID]
if !ok {
p.mu.Unlock()
return
}
retained.detached = true
registration, released := p.tryReleaseRetainedLocked(segmentID, retained)
p.mu.Unlock()
p.releaseRetained(registration, released)
}
func (p *delegatorGrowingSourceProvider) getRetained(segmentID int64) (segments.Segment, bool) {
p.mu.Lock()
defer p.mu.Unlock()
retained, ok := p.retained[segmentID]
if !ok {
return nil, false
}
return retained.segment, true
}
func (p *delegatorGrowingSourceProvider) releaseRetainedIfComplete(segmentID int64, targetOffset int64) {
p.mu.Lock()
retained, ok := p.retained[segmentID]
if !ok {
p.mu.Unlock()
return
}
if retained.committedOffset < targetOffset {
retained.committedOffset = targetOffset
}
registration, released := p.tryReleaseRetainedLocked(segmentID, retained)
p.mu.Unlock()
p.releaseRetained(registration, released)
}
func (p *delegatorGrowingSourceProvider) tryReleaseRetainedLocked(segmentID int64, retained *retainedGrowingFlushSource) (*syncmgr.GrowingSourceRegistration, *retainedGrowingFlushSource) {
if retained == nil ||
!retained.detached ||
retained.committedOffset < retained.targetOffset {
return nil, nil
}
delete(p.retained, segmentID)
return p.unregisterIfInactiveLocked(), retained
}
func (p *delegatorGrowingSourceProvider) releaseRetained(registration *syncmgr.GrowingSourceRegistration, retained *retainedGrowingFlushSource) {
if retained == nil {
syncmgr.DefaultGrowingSourceRegistry().Unregister(registration)
return
}
// Drop the retained pin first so Release can drain, then route through the
// segment manager's managed release path. The segment was already removed
// from the active maps by Detach, so release() will reconcile the
// on-releasing set, the segment gauge and the release callback. Calling
// segment.Release() directly here would leak the on-releasing set entry and
// keep Exist() reporting the segment forever.
retained.segment.Unpin()
p.segmentManager.ReleaseDetached(context.Background(), retained.segment)
syncmgr.DefaultGrowingSourceRegistry().Unregister(registration)
}
func (p *delegatorGrowingSourceProvider) acquireLease(segmentID int64) bool {
p.mu.Lock()
defer p.mu.Unlock()
if p.closing {
return false
}
if p.handoffOnly {
_, allowed := p.handoffAllowed[segmentID]
_, retained := p.retained[segmentID]
if !allowed && !retained {
return false
}
}
p.active++
return true
}
func (p *delegatorGrowingSourceProvider) releaseLease() {
p.mu.Lock()
defer p.mu.Unlock()
if p.active > 0 {
p.active--
}
if p.closing && p.active == 0 {
p.cond.Broadcast()
}
}
func (p *delegatorGrowingSourceProvider) isDeactivated() bool {
p.mu.Lock()
defer p.mu.Unlock()
return p.deactivated
}
func (p *delegatorGrowingSourceProvider) Deactivate() {
p.mu.Lock()
p.deactivated = true
registration := p.unregisterIfInactiveLocked()
p.mu.Unlock()
syncmgr.DefaultGrowingSourceRegistry().Unregister(registration)
}
func (p *delegatorGrowingSourceProvider) Close() {
p.mu.Lock()
p.closing = true
for p.active > 0 {
p.cond.Wait()
}
retained := p.retained
p.retained = make(map[int64]*retainedGrowingFlushSource)
p.releaseAllowed = make(map[int64]uint64)
p.releasePrepared = make(map[int64]int64)
p.handoffOnly = false
p.handoffAllowed = make(map[int64]struct{})
registration := p.registration
p.registration = nil
p.mu.Unlock()
for _, source := range retained {
source.segment.Unpin()
}
syncmgr.DefaultGrowingSourceRegistry().Unregister(registration)
}
func (p *delegatorGrowingSourceProvider) unregisterIfInactiveLocked() *syncmgr.GrowingSourceRegistration {
if !p.deactivated || len(p.retained) > 0 {
return nil
}
registration := p.registration
p.registration = nil
p.releaseAllowed = make(map[int64]uint64)
p.releasePrepared = make(map[int64]int64)
p.handoffOnly = false
p.handoffAllowed = make(map[int64]struct{})
return registration
}
func (p *delegatorGrowingSourceProvider) currentOffset(segment segments.Segment) int64 {
if segment == nil {
return 0
}
return segment.InsertCount()
}
type retainedGrowingFlushSource struct {
segment segments.Segment
targetOffset int64
committedOffset int64
detached bool
}
type delegatorGrowingFlushSource struct {
segmentID int64
segment segments.Segment
provider *delegatorGrowingSourceProvider
targetOffset int64
retained bool
once sync.Once
}
func (s *delegatorGrowingFlushSource) CurrentOffset() int64 {
if s.provider != nil {
return s.provider.currentOffset(s.segment)
}
if s.segment == nil {
return 0
}
return s.segment.InsertCount()
}
func (s *delegatorGrowingFlushSource) FlushGrowingData(ctx context.Context, startOffset, endOffset int64, config *syncmgr.GrowingFlushConfig) (*syncmgr.GrowingFlushResult, error) {
result, err := s.segment.FlushData(ctx, startOffset, endOffset, &segments.FlushConfig{
SegmentBasePath: config.SegmentBasePath,
PartitionBasePath: config.PartitionBasePath,
CollectionID: config.CollectionID,
PartitionID: config.PartitionID,
TextFieldIDs: config.TextFieldIDs,
TextLobPaths: config.TextLobPaths,
ReadVersion: config.ReadVersion,
})
if err != nil || result == nil {
return nil, err
}
return &syncmgr.GrowingFlushResult{
ManifestPath: result.ManifestPath,
NumRows: result.NumRows,
}, nil
}
func (s *delegatorGrowingFlushSource) Release() {
s.once.Do(func() {
if s.segment != nil {
s.segment.Unpin()
}
if s.provider != nil {
s.provider.releaseLease()
}
})
}
func (s *delegatorGrowingFlushSource) CommitGrowingFlush(targetOffset int64) {
if s.retained && s.provider != nil {
s.provider.releaseRetainedIfComplete(s.segmentID, targetOffset)
}
}
@@ -0,0 +1,321 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package delegator
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
)
func TestDelegatorGrowingSourceProviderCloseWaitsForSourceRelease(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
segment.EXPECT().Unpin().Once()
source, state := provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUsable, state)
require.NotNil(t, source)
closeDone := make(chan struct{})
go func() {
provider.Close()
close(closeDone)
}()
select {
case <-closeDone:
t.Fatal("provider close should wait for active growing source lease")
case <-time.After(50 * time.Millisecond):
}
source.Release()
require.Eventually(t, func() bool {
select {
case <-closeDone:
return true
default:
return false
}
}, time.Second, 10*time.Millisecond)
source, state = provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUnavailable, state)
require.Nil(t, source)
}
func TestDelegatorGrowingSourceProviderRetainedSource(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 0, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
require.NoError(t, err)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(nil).Twice()
segment.EXPECT().PinIfNotReleased().Return(nil).Twice()
segment.EXPECT().InsertCount().Return(int64(10)).Twice()
segment.EXPECT().Unpin().Times(3)
segmentManager.EXPECT().ReleaseDetached(context.Background(), segment).Once()
source, state := provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUsable, state)
require.NotNil(t, source)
source.Release()
source, state = provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUsable, state)
require.NotNil(t, source)
source.Release()
source.(syncmgr.GrowingFlushSourceCommitter).CommitGrowingFlush(10)
segmentManager.AssertNotCalled(t, "ReleaseDetached", context.Background(), segment)
provider.MarkReleaseDetached(1001)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(nil).Once()
source, state = provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUnavailable, state)
require.Nil(t, source)
}
func TestDelegatorGrowingSourceProviderUsesInsertCountAsOffset(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(12)).Twice()
segment.EXPECT().Unpin().Once()
source, state := provider.GetGrowingFlushSource(1001, 12, nil)
require.Equal(t, syncmgr.GrowingSourceUsable, state)
require.NotNil(t, source)
require.EqualValues(t, 12, source.CurrentOffset())
source.Release()
}
func TestDelegatorGrowingSourceProviderPrepareWaitsFence(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
waitedFence := uint64(0)
provider := newDelegatorGrowingSourceProvider(segmentManager, func(ctx context.Context, fenceTs uint64) error {
waitedFence = fenceTs
return nil
})
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 200, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
{SegmentID: 1002},
})
require.NoError(t, err)
require.EqualValues(t, 200, waitedFence)
require.True(t, provider.IsReleasePrepared(1001, 0))
require.True(t, provider.IsReleaseAllowed(1001, 0))
require.False(t, provider.IsReleasePrepared(1002, 200))
require.True(t, provider.IsReleaseAllowed(1002, 200))
require.False(t, provider.IsReleaseAllowed(1002, 201))
require.False(t, provider.IsReleasePrepared(1003, 200))
require.False(t, provider.IsReleaseAllowed(1003, 200))
segment.EXPECT().Unpin().Once()
segmentManager.EXPECT().ReleaseDetached(context.Background(), segment).Once()
provider.releaseRetainedIfComplete(1001, 10)
segmentManager.AssertNotCalled(t, "ReleaseDetached", context.Background(), segment)
provider.MarkReleaseDetached(1001)
}
func TestDelegatorGrowingSourceProviderHandoffOnlyRejectsNewSegmentsBeforeFence(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
waitFenceEntered := make(chan struct{})
releaseFence := make(chan struct{})
provider := newDelegatorGrowingSourceProvider(segmentManager, func(ctx context.Context, fenceTs uint64) error {
close(waitFenceEntered)
<-releaseFence
return nil
})
prepareDone := make(chan error, 1)
go func() {
prepareDone <- provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 200, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
}()
select {
case <-waitFenceEntered:
case <-time.After(time.Second):
t.Fatal("prepare did not enter fence wait")
}
source, state := provider.GetGrowingFlushSource(1002, 1, nil)
require.Equal(t, syncmgr.GrowingSourceUnavailable, state)
require.Nil(t, source)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
close(releaseFence)
require.NoError(t, <-prepareDone)
segment.EXPECT().Unpin().Once()
provider.Close()
}
func TestDelegatorGrowingSourceProviderDeactivatedOnlyServesRetainedSources(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
waitCount := 0
provider := newDelegatorGrowingSourceProvider(segmentManager, func(ctx context.Context, fenceTs uint64) error {
waitCount++
return nil
})
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 100, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
require.NoError(t, err)
require.Equal(t, 1, waitCount)
provider.Deactivate()
err = provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 200, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1002, TargetOffset: 5},
})
require.NoError(t, err)
require.Equal(t, 1, waitCount)
require.False(t, provider.IsReleaseAllowed(1002, 200))
require.False(t, provider.IsReleasePrepared(1002, 200))
require.True(t, provider.IsReleaseAllowed(1001, 100))
require.True(t, provider.IsReleasePrepared(1001, 100))
err = provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 200, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 11},
})
require.NoError(t, err)
require.False(t, provider.IsReleasePrepared(1001, 200))
segment.EXPECT().Unpin().Once()
segmentManager.EXPECT().ReleaseDetached(context.Background(), segment).Once()
provider.releaseRetainedIfComplete(1001, 10)
segmentManager.AssertNotCalled(t, "ReleaseDetached", context.Background(), segment)
provider.MarkReleaseDetached(1001)
}
func TestDelegatorGrowingSourceProviderReleasesWhenDetachedBeforeCommit(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 0, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
require.NoError(t, err)
provider.MarkReleaseDetached(1001)
segmentManager.AssertNotCalled(t, "ReleaseDetached", context.Background(), segment)
segment.EXPECT().Unpin().Once()
segmentManager.EXPECT().ReleaseDetached(context.Background(), segment).Once()
provider.releaseRetainedIfComplete(1001, 10)
}
func TestDelegatorGrowingSourceProviderPrepareRollbackOnFailure(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
behindSegment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(10)).Once()
segmentManager.EXPECT().GetGrowing(int64(1002)).Return(behindSegment).Once()
behindSegment.EXPECT().PinIfNotReleased().Return(nil).Once()
behindSegment.EXPECT().InsertCount().Return(int64(5)).Once()
behindSegment.EXPECT().Unpin().Once()
segment.EXPECT().Unpin().Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 200, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
{SegmentID: 1002, TargetOffset: 10},
})
require.Error(t, err)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(nil).Once()
source, state := provider.GetGrowingFlushSource(1001, 10, nil)
require.Equal(t, syncmgr.GrowingSourceUnavailable, state)
require.Nil(t, source)
}
func TestDelegatorGrowingSourceProviderRegisterRetainedSegmentNotFound(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
registry := syncmgr.NewGrowingSourceRegistry()
registration := registry.Register("ch", provider)
defer registry.Unregister(registration)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(nil).Once()
err := registry.PrepareGrowingSourceReleaseHandoff(context.Background(), "ch", 0, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
require.Error(t, err)
}
func TestDelegatorGrowingSourceProviderRegisterRetainedBehindTarget(t *testing.T) {
segmentManager := segments.NewMockSegmentManager(t)
segment := segments.NewMockSegment(t)
provider := newDelegatorGrowingSourceProvider(segmentManager, nil)
segmentManager.EXPECT().GetGrowing(int64(1001)).Return(segment).Once()
segment.EXPECT().PinIfNotReleased().Return(nil).Once()
segment.EXPECT().InsertCount().Return(int64(5)).Once()
segment.EXPECT().Unpin().Once()
err := provider.PrepareGrowingSourceReleaseHandoff(context.Background(), 0, []syncmgr.GrowingSourceReleaseHandoffSegment{
{SegmentID: 1001, TargetOffset: 10},
})
require.Error(t, err)
}
@@ -860,53 +860,6 @@ func (_c *MockShardDelegator_ProcessInsert_Call) RunAndReturn(run func(map[int64
return _c
}
// ProcessManualFlush provides a mock function with given fields: ctx, flushTs
func (_m *MockShardDelegator) ProcessManualFlush(ctx context.Context, flushTs uint64) error {
ret := _m.Called(ctx, flushTs)
if len(ret) == 0 {
panic("no return value specified for ProcessManualFlush")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, uint64) error); ok {
r0 = rf(ctx, flushTs)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockShardDelegator_ProcessManualFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProcessManualFlush'
type MockShardDelegator_ProcessManualFlush_Call struct {
*mock.Call
}
// ProcessManualFlush is a helper method to define mock.On call
// - ctx context.Context
// - flushTs uint64
func (_e *MockShardDelegator_Expecter) ProcessManualFlush(ctx interface{}, flushTs interface{}) *MockShardDelegator_ProcessManualFlush_Call {
return &MockShardDelegator_ProcessManualFlush_Call{Call: _e.mock.On("ProcessManualFlush", ctx, flushTs)}
}
func (_c *MockShardDelegator_ProcessManualFlush_Call) Run(run func(ctx context.Context, flushTs uint64)) *MockShardDelegator_ProcessManualFlush_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uint64))
})
return _c
}
func (_c *MockShardDelegator_ProcessManualFlush_Call) Return(_a0 error) *MockShardDelegator_ProcessManualFlush_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockShardDelegator_ProcessManualFlush_Call) RunAndReturn(run func(context.Context, uint64) error) *MockShardDelegator_ProcessManualFlush_Call {
_c.Call.Return(run)
return _c
}
// Query provides a mock function with given fields: ctx, req
func (_m *MockShardDelegator) Query(ctx context.Context, req *querypb.QueryRequest) ([]*internalpb.RetrieveResults, error) {
ret := _m.Called(ctx, req)
+3 -4
View File
@@ -150,15 +150,14 @@ func (fNode *filterNode) filtrate(c *Collection, msg msgstream.TsMsg) error {
}
return nil
case commonpb.MsgType_ManualFlush:
// Handle ManualFlush message for TEXT collections
// This triggers immediate flush of Growing Segment data
// ManualFlush is handled by StreamingNode WAL flusher (fence + persist).
// QueryNode only consumes the barrier so the pipeline advances.
manualFlushMsg := msg.(*adaptor.ManualFlushMessageBody)
header := manualFlushMsg.ManualFlushMessage.Header()
if header.GetCollectionId() != fNode.collectionID {
return merr.WrapErrCollectionNotFound(header.GetCollectionId())
}
// Trigger flush via delegator
return fNode.delegator.ProcessManualFlush(context.Background(), header.GetFlushTs())
return nil
default:
return merr.WrapErrParameterInvalid("msgType is Insert or Delete", "not")
}
+1 -2
View File
@@ -71,8 +71,7 @@ func (msg *insertNodeMsg) append(taskMsg msgstream.TsMsg) error {
msg.schemaVersion = taskMsg.BeginTs()
}
case commonpb.MsgType_ManualFlush:
// ManualFlush is already handled in filterNode.filtrate() via delegator.ProcessManualFlush().
// No-op here since ManualFlush doesn't carry insert/delete data.
// ManualFlush is consumed in filterNode.filtrate(); no insert/delete payload here.
default:
return merr.WrapErrParameterInvalid("msgType is Insert or Delete", "not")
}
@@ -36,8 +36,8 @@ type BatchInfo struct {
}
// CheckpointTracker tracks the mapping between segment row offsets and WAL positions.
// it is used by GrowingFlushManager to determine the correct checkpoint when flushing
// a range of rows from a Growing Segment.
// It is legacy QueryNode-side state kept for compatibility while TEXT flush
// ownership moves to the WAL flusher / WriteBuffer path.
//
// design:
// - each Insert operation records a BatchInfo with (endOffset, position)
@@ -1,589 +0,0 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package segments
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/cockroachdb/errors"
"go.uber.org/zap"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
milvuscommon "github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/metautil"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
)
// BinlogSaver is a minimal interface for saving binlog paths to DataCoord.
// This avoids depending on the full broker.Broker interface.
type BinlogSaver interface {
SaveBinlogPaths(ctx context.Context, req *datapb.SaveBinlogPathsRequest) error
}
// GrowingFlushManager manages the incremental flush of Growing Segments for TEXT collections.
// it periodically checks sync policies and triggers flush operations when conditions are met.
//
// key responsibilities:
// - track Growing Segments and their unflushed data via CheckpointTracker
// - apply sync policies to determine which segments to flush
// - extract data from Growing Segments and write to binlog storage
// - update metadata via DataCoord's SaveBinlogPaths API
// - update channel checkpoints
type GrowingFlushManager struct {
collectionID int64
channelName string
schema *schemapb.CollectionSchema
// dependencies
collectionManager CollectionManager
segmentManager SegmentManager
binlogSaver BinlogSaver
chunkManager storage.ChunkManager
checkpointTracker *CheckpointTracker
// sync policies
policies []GrowingSyncPolicy
// configuration
flushInterval time.Duration
// sync mutex to prevent concurrent sync on the same segment
syncMu sync.Map // map[int64]*sync.Mutex - segmentID -> mutex
// sealedSegments tracks segments that have already been sealed via ForceSyncAndSeal.
// This prevents duplicate SaveBinlogPaths(isFlush=true) requests when the user
// calls flush() multiple times, which can resurrect Dropped segments and cause
// duplicate data after compaction.
sealedSegments sync.Map // map[int64]struct{} - segmentID -> struct{}
// control
stopCh chan struct{}
stopOnce sync.Once
wg sync.WaitGroup
// logger
logger *log.MLogger
}
// GrowingFlushManagerOption is a function to configure GrowingFlushManager.
type GrowingFlushManagerOption func(*GrowingFlushManager)
// WithFlushInterval sets the flush check interval.
func WithFlushInterval(interval time.Duration) GrowingFlushManagerOption {
return func(m *GrowingFlushManager) {
m.flushInterval = interval
}
}
// WithSyncPolicies sets custom sync policies.
func WithSyncPolicies(policies ...GrowingSyncPolicy) GrowingFlushManagerOption {
return func(m *GrowingFlushManager) {
m.policies = policies
}
}
// NewGrowingFlushManager creates a new GrowingFlushManager instance.
func NewGrowingFlushManager(
collectionID int64,
channelName string,
schema *schemapb.CollectionSchema,
collectionManager CollectionManager,
segmentManager SegmentManager,
binlogSaver BinlogSaver,
chunkManager storage.ChunkManager,
checkpointTracker *CheckpointTracker,
opts ...GrowingFlushManagerOption,
) *GrowingFlushManager {
m := &GrowingFlushManager{
collectionID: collectionID,
channelName: channelName,
schema: schema,
collectionManager: collectionManager,
segmentManager: segmentManager,
binlogSaver: binlogSaver,
chunkManager: chunkManager,
checkpointTracker: checkpointTracker,
stopCh: make(chan struct{}),
logger: log.With(
zap.Int64("collectionID", collectionID),
zap.String("channel", channelName),
),
}
// apply options
for _, opt := range opts {
opt(m)
}
// set defaults
if m.flushInterval == 0 {
m.flushInterval = 100 * time.Millisecond
}
if len(m.policies) == 0 {
m.policies = m.defaultPolicies()
}
return m
}
// defaultPolicies returns the default sync policies.
func (m *GrowingFlushManager) defaultPolicies() []GrowingSyncPolicy {
staleDuration := paramtable.Get().DataNodeCfg.SyncPeriod.GetAsDuration(time.Second)
return []GrowingSyncPolicy{
GetGrowingFullBufferPolicy(),
GetGrowingStaleBufferPolicy(staleDuration),
}
}
// Start begins the background flush checking loop.
func (m *GrowingFlushManager) Start(ctx context.Context) {
m.wg.Add(1)
go m.backgroundLoop(ctx)
m.logger.Info("GrowingFlushManager started")
}
// Stop signals the manager to stop and waits for completion.
// Safe to call multiple times.
func (m *GrowingFlushManager) Stop() {
m.stopOnce.Do(func() {
close(m.stopCh)
})
m.wg.Wait()
m.logger.Info("GrowingFlushManager stopped")
}
// backgroundLoop periodically checks sync policies and triggers flushes.
func (m *GrowingFlushManager) backgroundLoop(ctx context.Context) {
defer m.wg.Done()
ticker := time.NewTicker(m.flushInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-m.stopCh:
return
case <-ticker.C:
m.checkAndTriggerSync(ctx)
}
}
}
// checkAndTriggerSync checks all policies and triggers sync for matching segments.
func (m *GrowingFlushManager) checkAndTriggerSync(ctx context.Context) {
// get all growing segments
segments := m.getGrowingSegments()
if len(segments) == 0 {
return
}
// wrap segments as GrowingSegmentBuffer for policy evaluation
buffers := m.wrapAsBuffers(segments)
currentTs := tsoutil.ComposeTSByTime(time.Now(), 0)
// track which segments to sync (dedup across policies)
syncSet := make(map[int64]string) // segmentID -> reason
// evaluate each policy
for _, policy := range m.policies {
segIDs := policy.SelectSegments(buffers, currentTs)
for _, segID := range segIDs {
if _, exists := syncSet[segID]; !exists {
syncSet[segID] = policy.Reason()
}
}
}
// trigger sync for each selected segment
for segID, reason := range syncSet {
segment := m.findSegment(segments, segID)
if segment == nil {
continue
}
m.triggerSync(ctx, segment, reason)
}
}
// getGrowingSegments returns all growing segments from the segment manager.
func (m *GrowingFlushManager) getGrowingSegments() []Segment {
// get segment IDs being tracked
trackedIDs := m.checkpointTracker.GetSegmentIDs()
if len(trackedIDs) == 0 {
return nil
}
segments := make([]Segment, 0, len(trackedIDs))
for _, segID := range trackedIDs {
seg := m.segmentManager.GetGrowing(segID)
if seg != nil {
segments = append(segments, seg)
}
}
return segments
}
// wrapAsBuffers wraps segments as GrowingSegmentBuffer for policy evaluation.
func (m *GrowingFlushManager) wrapAsBuffers(segments []Segment) []*GrowingSegmentBuffer {
buffers := make([]*GrowingSegmentBuffer, 0, len(segments))
for _, seg := range segments {
buf := NewGrowingSegmentBuffer(seg, m.checkpointTracker)
if buf.HasUnflushedData() {
buffers = append(buffers, buf)
}
}
return buffers
}
// findSegment finds a segment by ID from the list.
func (m *GrowingFlushManager) findSegment(segments []Segment, segID int64) Segment {
for _, seg := range segments {
if seg.ID() == segID {
return seg
}
}
return nil
}
// triggerSync initiates a sync operation for the given segment.
func (m *GrowingFlushManager) triggerSync(ctx context.Context, segment Segment, reason string) {
segID := segment.ID()
log := m.logger.With(
zap.Int64("segmentID", segID),
zap.String("reason", reason),
)
// get or create mutex for this segment to prevent concurrent sync
muInterface, _ := m.syncMu.LoadOrStore(segID, &sync.Mutex{})
mu := muInterface.(*sync.Mutex)
// try to acquire lock without blocking
if !mu.TryLock() {
log.Debug("sync already in progress, skipping")
return
}
log.Info("triggering growing segment sync")
// async sync to avoid blocking the check loop
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer mu.Unlock()
if err := m.doSync(ctx, segment, false); err != nil {
log.Error("growing segment sync failed", zap.Error(err))
}
}()
}
// doSync performs the actual sync operation for a segment.
// when isSeal is true, this is the final flush for the segment (user flush/seal),
// and SaveBinlogPaths will set Flushed=true to trigger DataCoord handoff.
func (m *GrowingFlushManager) doSync(ctx context.Context, segment Segment, isSeal bool) error {
segID := segment.ID()
log := m.logger.With(zap.Int64("segmentID", segID))
// 1. get unflushed data range
// Use GetMaxEndOffset (from CheckpointTracker) instead of segment.RowNum() to
// determine how far to flush. This ensures we only flush data that has a
flushedOffset := m.checkpointTracker.GetFlushedOffset(segID)
currentOffset := m.checkpointTracker.GetMaxEndOffset(segID)
if flushedOffset >= currentOffset {
if isSeal {
lastManifest := m.checkpointTracker.GetAcknowledgedManifest(segID)
if lastManifest == "" {
log.Info("empty segment with no manifest, skip seal",
zap.Int64("flushedOffset", flushedOffset))
return nil
}
log.Info("no unflushed data, but sealing segment",
zap.String("lastManifest", lastManifest),
zap.Int64("flushedRows", flushedOffset))
return m.saveManifestAndCheckpoint(ctx, segment, lastManifest, nil, flushedOffset, true)
}
log.Debug("no unflushed data")
return nil
}
unflushedRows := currentOffset - flushedOffset
log.Info("starting sync",
zap.Int64("flushedOffset", flushedOffset),
zap.Int64("currentOffset", currentOffset),
zap.Int64("unflushedRows", unflushedRows),
)
// 2. get checkpoint position for this sync
checkpoint := m.checkpointTracker.GetCheckpoint(segID, currentOffset)
// 3. flush data directly via C++ milvus-storage (unified interface)
// Use the last acknowledged manifest version as read_version.
flushConfig, err := m.buildFlushConfig(segment)
if err != nil {
return err
}
flushConfig.ReadVersion = m.checkpointTracker.GetAcknowledgedVersion(segID)
flushResult, err := segment.FlushData(ctx, flushedOffset, currentOffset, flushConfig)
if err != nil {
return errors.Wrap(err, "failed to flush data via C++ milvus-storage")
}
if flushResult == nil || flushResult.ManifestPath == "" {
if isSeal {
lastManifest := m.checkpointTracker.GetAcknowledgedManifest(segID)
if lastManifest == "" {
log.Warn("FlushData returned no manifest and no prior manifest exists, skip seal")
return nil
}
// Use flushedOffset (not currentOffset) — the last manifest only contains
// data up to flushedOffset. Using currentOffset would over-report rows,
// causing sort compaction to fail with "unexpected row count".
log.Warn("no data flushed but seal requested, using last manifest",
zap.String("lastManifest", lastManifest),
zap.Int64("flushedOffset", flushedOffset),
zap.Int64("currentOffset", currentOffset))
return m.saveManifestAndCheckpoint(ctx, segment, lastManifest, checkpoint, flushedOffset, true)
}
log.Debug("no data flushed or no manifest path returned")
return nil
}
// 4. save manifest path and checkpoint to DataCoord
// In Storage V3 FFI mode, only manifest path is needed (all file info is in manifest)
if err := m.saveManifestAndCheckpoint(ctx, segment, flushResult.ManifestPath, checkpoint, currentOffset, isSeal); err != nil {
return err
}
// 5. update flushed offset AND acknowledged version
// Both updates happen ONLY after SaveBinlogPaths succeeds.
m.checkpointTracker.UpdateFlushedOffset(segID, currentOffset)
m.checkpointTracker.UpdateAcknowledgedManifest(segID, flushResult.ManifestPath)
log.Info("sync completed",
zap.Int64("flushedRows", flushResult.NumRows),
zap.String("manifest", flushResult.ManifestPath),
)
return nil
}
// buildFlushConfig builds FlushConfig for C++ FFI call.
// Go layer only constructs paths and config, all logic is in C++ side.
func (m *GrowingFlushManager) buildFlushConfig(segment Segment) (*FlushConfig, error) {
// Build paths for C++ side
// Segment path: {root}/insert_log/{coll}/{part}/{seg}
segmentBasePath := m.chunkManager.RootPath() + "/insert_log/" +
metautil.JoinIDPath(m.collectionID, segment.Partition(), segment.ID())
// Partition path: {root}/insert_log/{coll}/{part}
partitionBasePath := m.chunkManager.RootPath() + "/insert_log/" +
metautil.JoinIDPath(m.collectionID, segment.Partition())
// Collect TEXT field IDs and LOB paths from schema
var textFieldIDs []int64
var textLobPaths []string
if m.schema != nil {
for _, field := range m.schema.GetFields() {
if field.GetDataType() == schemapb.DataType_Text {
fieldID := field.GetFieldID()
textFieldIDs = append(textFieldIDs, fieldID)
// LOB path: {partition_path}/lobs/{field_id}
lobPath := fmt.Sprintf("%s/lobs/%d", partitionBasePath, fieldID)
textLobPaths = append(textLobPaths, lobPath)
}
}
}
globalWriterFormat := paramtable.Get().DataNodeCfg.StorageFormat.GetValue()
writerFormat, err := m.resolveFlushWriterFormat(segment, globalWriterFormat)
if err != nil {
return nil, err
}
return &FlushConfig{
SegmentBasePath: segmentBasePath,
PartitionBasePath: partitionBasePath,
CollectionID: m.collectionID,
PartitionID: segment.Partition(),
TextFieldIDs: textFieldIDs,
TextLobPaths: textLobPaths,
WriterFormat: writerFormat,
}, nil
}
func (m *GrowingFlushManager) resolveFlushWriterFormat(segment Segment, globalWriterFormat string) (string, error) {
if m.checkpointTracker == nil {
return globalWriterFormat, nil
}
manifest := m.checkpointTracker.GetAcknowledgedManifest(segment.ID())
if manifest == "" {
return globalWriterFormat, nil
}
format, err := packed.ResolveManifestSingleWriterFormat(manifest, packed.CreateStorageConfig(), m.flushWriterColumns(), globalWriterFormat)
if err != nil {
return "", err
}
return format, nil
}
func (m *GrowingFlushManager) flushWriterColumns() []string {
seen := make(map[string]struct{})
columns := []string{
strconv.FormatInt(milvuscommon.RowIDField, 10),
strconv.FormatInt(milvuscommon.TimeStampField, 10),
}
seen[columns[0]] = struct{}{}
seen[columns[1]] = struct{}{}
if m.schema == nil {
return columns
}
for _, field := range m.schema.GetFields() {
column := strconv.FormatInt(field.GetFieldID(), 10)
if _, ok := seen[column]; ok {
continue
}
seen[column] = struct{}{}
columns = append(columns, column)
}
return columns
}
// saveManifestAndCheckpoint saves manifest path and checkpoint to DataCoord.
// In Storage V3 FFI mode, only manifest path is needed - all file information
// (binlog paths, column groups, etc.) is contained in the manifest file.
// C++ side has already written all files and created the manifest.
func (m *GrowingFlushManager) saveManifestAndCheckpoint(
ctx context.Context,
segment Segment,
manifestPath string,
checkpoint *msgpb.MsgPosition,
numRows int64,
isSeal bool,
) error {
if m.binlogSaver == nil {
return errors.New("binlogSaver is nil, cannot save manifest and checkpoint")
}
segID := segment.ID()
// In Storage V3 FFI mode, SaveBinlogPaths is only used to:
// 1. Save manifest path (UpdateManifest)
// 2. Save checkpoint (UpdateCheckPoint)
// 3. Save start position (if needed)
// All file paths are in manifest, no need to pass Field2BinlogPaths
req := &datapb.SaveBinlogPathsRequest{
Base: &commonpb.MsgBase{},
SegmentID: segID,
CollectionID: m.collectionID,
PartitionID: segment.Partition(),
Flushed: isSeal, // true when sealing, false for incremental flush
WithFullBinlogs: false, // incremental mode
ManifestPath: manifestPath, // Storage V3 manifest path (from C++ side)
StorageVersion: storage.StorageV3,
}
if checkpoint != nil {
req.CheckPoints = []*datapb.CheckPoint{{
SegmentID: segID,
Position: checkpoint,
NumOfRows: numRows,
}}
}
return m.binlogSaver.SaveBinlogPaths(ctx, req)
}
// ForceSync forces an immediate sync of all unflushed data for a segment.
// this is typically called when a segment is being sealed or dropped.
func (m *GrowingFlushManager) ForceSync(ctx context.Context, segID int64) error {
segment := m.segmentManager.GetGrowing(segID)
if segment == nil {
return nil // segment not found, nothing to sync
}
// acquire per-segment mutex to prevent concurrent doSync with background triggerSync
muInterface, _ := m.syncMu.LoadOrStore(segID, &sync.Mutex{})
mu := muInterface.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
return m.doSync(ctx, segment, false)
}
// ForceSyncAndSeal forces an immediate sync and marks the segment as flushed/sealed.
// This is called when the segment is being sealed (user flush command).
// After this call, DataCoord will mark the segment as Flushed and trigger handoff.
//
// This method is idempotent: once a segment has been successfully sealed, subsequent
// calls are no-ops. This prevents duplicate SaveBinlogPaths(isFlush=true) requests
// when flush() is called multiple times, which would otherwise resurrect compacted
// segments via the Dropped→Flushed state transition race and cause duplicate data.
func (m *GrowingFlushManager) ForceSyncAndSeal(ctx context.Context, segID int64) error {
// Idempotency check: skip segments that have already been sealed.
if _, sealed := m.sealedSegments.Load(segID); sealed {
m.logger.Debug("segment already sealed, skipping duplicate ForceSyncAndSeal",
zap.Int64("segmentID", segID))
return nil
}
segment := m.segmentManager.GetGrowing(segID)
if segment == nil {
return nil // segment not found, nothing to sync
}
// acquire per-segment mutex to prevent concurrent doSync with background triggerSync
muInterface, _ := m.syncMu.LoadOrStore(segID, &sync.Mutex{})
mu := muInterface.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
// Double-check after acquiring lock, another goroutine may have sealed it.
if _, sealed := m.sealedSegments.Load(segID); sealed {
return nil
}
if err := m.doSync(ctx, segment, true); err != nil {
return err
}
m.sealedSegments.Store(segID, struct{}{})
return nil
}
// RemoveSealedSegment clears the sealed state for a segment.
// This should be called when the segment is released from QueryNode.
func (m *GrowingFlushManager) RemoveSealedSegment(segID int64) {
m.sealedSegments.Delete(segID)
}
// GetCheckpointTracker returns the checkpoint tracker for external access.
func (m *GrowingFlushManager) GetCheckpointTracker() *CheckpointTracker {
return m.checkpointTracker
}
@@ -1,931 +0,0 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package segments
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/mocks/mock_storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
)
func TestNewGrowingFlushManager(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_Text},
},
}
tracker := NewCheckpointTracker()
t.Run("default configuration", func(t *testing.T) {
m := NewGrowingFlushManager(
1, // collectionID
"test-channel", // channelName
schema, // schema
nil, // collectionManager
nil, // segmentManager
nil, // broker
nil, // chunkManager
tracker, // checkpointTracker
)
assert.NotNil(t, m)
assert.Equal(t, int64(1), m.collectionID)
assert.Equal(t, "test-channel", m.channelName)
assert.Equal(t, schema, m.schema)
assert.Equal(t, tracker, m.checkpointTracker)
assert.Equal(t, 100*time.Millisecond, m.flushInterval)
assert.True(t, len(m.policies) > 0) // Default policies should be set
})
t.Run("custom configuration", func(t *testing.T) {
customPolicy := GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, ts uint64) []int64 { return nil },
reason: "custom",
}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
nil,
tracker,
WithFlushInterval(500*time.Millisecond),
WithSyncPolicies(customPolicy),
)
assert.Equal(t, 500*time.Millisecond, m.flushInterval)
assert.Len(t, m.policies, 1)
assert.Equal(t, "custom", m.policies[0].Reason())
})
}
func TestGrowingFlushManager_StartStop(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
}
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil,
nil,
tracker,
WithFlushInterval(10*time.Millisecond),
)
ctx := context.Background()
// Start the manager
m.Start(ctx)
// Let it run briefly
time.Sleep(50 * time.Millisecond)
// Stop should complete without blocking
done := make(chan struct{})
go func() {
m.Stop()
close(done)
}()
select {
case <-done:
// Success - stopped cleanly
case <-time.After(time.Second):
t.Fatal("Stop() did not complete in time")
}
}
func TestGrowingFlushManager_WrapAsBuffers(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
// Record batches so segments have unflushed data (needed by HasUnflushedData check)
pos := &msgpb.MsgPosition{Timestamp: 100}
tracker.RecordBatch(1001, 50, pos)
tracker.RecordBatch(1002, 50, pos)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
nil,
tracker,
)
// Create mock segments with all necessary expectations
seg1 := NewMockSegment(t)
seg1.EXPECT().ID().Return(int64(1001)).Maybe()
seg1.EXPECT().RowNum().Return(int64(50)).Maybe()
seg2 := NewMockSegment(t)
seg2.EXPECT().ID().Return(int64(1002)).Maybe()
seg2.EXPECT().RowNum().Return(int64(50)).Maybe()
segments := []Segment{seg1, seg2}
buffers := m.wrapAsBuffers(segments)
assert.Len(t, buffers, 2)
assert.Equal(t, int64(1001), buffers[0].SegmentID())
assert.Equal(t, int64(1002), buffers[1].SegmentID())
}
func TestGrowingFlushManager_GetGrowingSegments(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil,
nil,
tracker,
)
t.Run("no tracked segments", func(t *testing.T) {
segments := m.getGrowingSegments()
assert.Len(t, segments, 0)
})
t.Run("with tracked segments", func(t *testing.T) {
// Record batches to track segments
pos := &msgpb.MsgPosition{Timestamp: 100}
tracker.RecordBatch(1001, 50, pos)
tracker.RecordBatch(1002, 50, pos)
// Setup mock expectations
seg1 := NewMockSegment(t)
seg2 := NewMockSegment(t)
segMgr.EXPECT().GetGrowing(int64(1001)).Return(seg1).Once()
segMgr.EXPECT().GetGrowing(int64(1002)).Return(seg2).Once()
segments := m.getGrowingSegments()
assert.Len(t, segments, 2)
})
t.Run("segment not found in manager", func(t *testing.T) {
// Remove old tracker data and create fresh tracker
tracker = NewCheckpointTracker()
m.checkpointTracker = tracker
pos := &msgpb.MsgPosition{Timestamp: 100}
tracker.RecordBatch(9999, 50, pos)
// Create a fresh mock for this test
segMgr2 := NewMockSegmentManager(t)
m.segmentManager = segMgr2
// Segment doesn't exist
segMgr2.EXPECT().GetGrowing(int64(9999)).Return(nil).Once()
segments := m.getGrowingSegments()
assert.Len(t, segments, 0)
})
}
func TestGrowingFlushManager_FindSegment(t *testing.T) {
paramtable.Init()
m := &GrowingFlushManager{}
seg1 := NewMockSegment(t)
seg1.EXPECT().ID().Return(int64(1001))
seg2 := NewMockSegment(t)
seg2.EXPECT().ID().Return(int64(1002))
segments := []Segment{seg1, seg2}
t.Run("found", func(t *testing.T) {
found := m.findSegment(segments, 1001)
assert.Equal(t, seg1, found)
})
t.Run("not found", func(t *testing.T) {
found := m.findSegment(segments, 9999)
assert.Nil(t, found)
})
}
func TestGrowingFlushManager_CheckAndTriggerSync(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
// Create a custom policy that always selects segment 1001
customPolicy := GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, ts uint64) []int64 {
for _, buf := range buffers {
if buf.SegmentID() == 1001 {
return []int64{1001}
}
}
return nil
},
reason: "test policy",
}
// Create mock chunk manager for buildFlushConfig
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil, // broker - needed for triggerSync but we won't call it in this test
mockCM, // chunkManager - needed for buildFlushConfig
tracker,
WithSyncPolicies(customPolicy),
)
// Record batches to track segments
pos := &msgpb.MsgPosition{Timestamp: 100}
tracker.RecordBatch(1001, 50, pos)
// Setup mock segment
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().MemSize().Return(int64(1024)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
// FlushData will be called in doSync goroutine
seg.EXPECT().FlushData(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Maybe()
segMgr.EXPECT().GetGrowing(int64(1001)).Return(seg).Maybe()
// checkAndTriggerSync will trigger sync in a goroutine.
// The actual sync will fail gracefully (no FlushResult returned)
ctx := context.Background()
m.checkAndTriggerSync(ctx)
// Wait briefly for the async goroutine to run
time.Sleep(50 * time.Millisecond)
}
func TestGrowingFlushManager_DefaultPolicies(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
nil,
NewCheckpointTracker(),
)
policies := m.defaultPolicies()
// Should have at least FullBufferPolicy and StaleBufferPolicy
assert.True(t, len(policies) >= 2)
reasons := make([]string, len(policies))
for i, p := range policies {
reasons[i] = p.Reason()
}
assert.Contains(t, reasons, "buffer full")
assert.Contains(t, reasons, "buffer stale")
}
// Test segment mutex for concurrent sync prevention
func TestGrowingFlushManager_SegmentMutex(t *testing.T) {
paramtable.Init()
m := &GrowingFlushManager{}
segID := int64(1001)
// Get or create mutex
mutexI, _ := m.syncMu.LoadOrStore(segID, &sync.Mutex{})
mu := mutexI.(*sync.Mutex)
// First lock should succeed
locked := mu.TryLock()
assert.True(t, locked)
// Second lock should fail (mutex is held)
locked2 := mu.TryLock()
assert.False(t, locked2)
// After unlock, lock should succeed again
mu.Unlock()
locked3 := mu.TryLock()
assert.True(t, locked3)
mu.Unlock()
}
// Mock helpers for testing
type testSyncPolicy struct {
mock.Mock
}
func (p *testSyncPolicy) SelectSegments(buffers []*GrowingSegmentBuffer, ts uint64) []int64 {
args := p.Called(buffers, ts)
return args.Get(0).([]int64)
}
func (p *testSyncPolicy) Reason() string {
args := p.Called()
return args.String(0)
}
func TestGrowingFlushManager_BuildFlushConfig(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar},
},
}
tracker := NewCheckpointTracker()
// Create mock chunk manager
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
m := NewGrowingFlushManager(
1, // collectionID
"test-channel", // channelName
schema, // schema
nil, // collectionManager
nil, // segmentManager
nil, // broker
mockCM, // chunkManager
tracker, // checkpointTracker
)
// Create mock segment
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
config, err := m.buildFlushConfig(seg)
assert.NoError(t, err)
assert.NotNil(t, config)
assert.Contains(t, config.SegmentBasePath, "/test/root")
assert.Contains(t, config.SegmentBasePath, "insert_log")
assert.Contains(t, config.PartitionBasePath, "/test/root")
assert.Equal(t, int64(1), config.CollectionID)
assert.Equal(t, int64(10), config.PartitionID)
assert.Equal(t, "parquet", config.WriterFormat)
}
func TestGrowingFlushManager_BuildFlushConfigKeepsGlobalFormatAndUsesManifestForSinglePolicy(t *testing.T) {
paramtable.Init()
params := paramtable.Get()
rootPath := t.TempDir()
assert.NoError(t, params.Save(params.CommonCfg.StorageType.Key, "local"))
assert.NoError(t, params.Save(params.LocalStorageCfg.Path.Key, rootPath))
assert.NoError(t, params.Save(params.DataNodeCfg.StorageFormat.Key, "parquet"))
t.Cleanup(func() {
_ = params.Reset(params.CommonCfg.StorageType.Key)
_ = params.Reset(params.LocalStorageCfg.Path.Key)
_ = params.Reset(params.DataNodeCfg.StorageFormat.Key)
})
manifestPath, err := packed.CreateManifestForSegment(
"files/querynode_format/segment",
[]string{"0", "1", "100", "101"},
"parquet",
[]packed.Fragment{{FilePath: "files/querynode_format/segment/_data/0.parquet", StartRow: 0, EndRow: 1, RowCount: 1}},
packed.CreateStorageConfig(),
)
assert.NoError(t, err)
assert.NoError(t, params.Save(params.DataNodeCfg.StorageFormat.Key, "vortex"))
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar},
},
}
tracker := NewCheckpointTracker()
tracker.InitSegmentWithManifest(1001, 1, manifestPath)
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return(rootPath).Maybe()
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
mockCM,
tracker,
)
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
config, err := m.buildFlushConfig(seg)
assert.NoError(t, err)
assert.NotNil(t, config)
assert.Equal(t, "parquet", config.WriterFormat)
}
func TestGrowingFlushManager_DoSync_NoUnflushedData(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil,
nil,
tracker,
)
// Create mock segment with no unflushed data
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
// Set flushed offset to equal row count (no unflushed data)
tracker.UpdateFlushedOffset(1001, 100)
// doSync should return nil when no data to flush
err := m.doSync(context.Background(), seg, false)
assert.NoError(t, err)
}
func TestGrowingFlushManager_DoSync_WithUnflushedData(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
mockCM,
tracker,
)
// Create mock segment with unflushed data
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
// Set flushed offset less than row count (has unflushed data)
tracker.UpdateFlushedOffset(1001, 50)
// Record batch to get checkpoint
pos := &msgpb.MsgPosition{Timestamp: 200}
tracker.RecordBatch(1001, 100, pos)
// FlushData will be called
flushResult := &FlushResult{
ManifestPath: "/test/manifest.json",
NumRows: 50,
}
seg.EXPECT().FlushData(mock.Anything, int64(50), int64(100), mock.Anything).Return(flushResult, nil).Once()
// Without broker, saveManifestAndCheckpoint will fail
// but the test should still exercise the main logic
err := m.doSync(context.Background(), seg, false)
// Expected to fail because broker is nil
assert.Error(t, err)
}
func TestGrowingFlushManager_ForceSync(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil,
nil,
tracker,
)
t.Run("segment not found", func(t *testing.T) {
segMgr.EXPECT().GetGrowing(int64(9999)).Return(nil).Once()
err := m.ForceSync(context.Background(), 9999)
assert.NoError(t, err) // should return nil when segment not found
})
t.Run("segment exists but no unflushed data", func(t *testing.T) {
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
segMgr.EXPECT().GetGrowing(int64(1001)).Return(seg).Once()
// Set flushed offset equal to row count
tracker.UpdateFlushedOffset(1001, 100)
err := m.ForceSync(context.Background(), 1001)
assert.NoError(t, err)
})
}
func TestGrowingFlushManager_TriggerSync_AlreadyInProgress(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
nil,
nil,
NewCheckpointTracker(),
)
// Create mock segment
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
// Acquire the mutex first to simulate sync in progress
muInterface, _ := m.syncMu.LoadOrStore(int64(1001), &sync.Mutex{})
mu := muInterface.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
// triggerSync should skip because sync is already in progress
// (this is tested by not expecting any FlushData calls)
m.triggerSync(context.Background(), seg, "test")
// Give time for goroutine to potentially start
time.Sleep(10 * time.Millisecond)
// If FlushData was not called, the test passes
// The mock would fail if FlushData was called without expectation
}
func TestGrowingFlushManager_GetCheckpointTracker(t *testing.T) {
paramtable.Init()
tracker := NewCheckpointTracker()
m := NewGrowingFlushManager(
1,
"test-channel",
nil,
nil,
nil,
nil,
nil,
tracker,
)
assert.Equal(t, tracker, m.GetCheckpointTracker())
}
// MockBinlogSaver is a testify mock implementing the BinlogSaver interface.
type MockBinlogSaver struct {
mock.Mock
}
func (m *MockBinlogSaver) SaveBinlogPaths(ctx context.Context, req *datapb.SaveBinlogPathsRequest) error {
args := m.Called(ctx, req)
return args.Error(0)
}
func TestGrowingFlushManager_ForceSyncAndSeal(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
}
t.Run("segment not found returns nil", func(t *testing.T) {
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
nil,
nil,
tracker,
)
segMgr.EXPECT().GetGrowing(int64(9999)).Return(nil).Once()
err := m.ForceSyncAndSeal(context.Background(), 9999)
assert.NoError(t, err)
})
t.Run("calls doSync with isSeal true", func(t *testing.T) {
tracker := NewCheckpointTracker()
segMgr := NewMockSegmentManager(t)
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
binlogSaver := &MockBinlogSaver{}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
segMgr,
binlogSaver,
mockCM,
tracker,
)
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
segMgr.EXPECT().GetGrowing(int64(1001)).Return(seg).Once()
// Set up tracker with unflushed data: flushed offset = 0, record batch up to 100
pos := &msgpb.MsgPosition{ChannelName: "test-channel", MsgID: []byte{1}, Timestamp: 100}
tracker.RecordBatch(1001, 100, pos)
// Mock FlushData to return a valid result
flushResult := &FlushResult{
ManifestPath: "/test/manifest.json",
NumRows: 100,
}
seg.EXPECT().FlushData(mock.Anything, int64(0), int64(100), mock.Anything).Return(flushResult, nil).Once()
// Capture the SaveBinlogPaths request and assert Flushed=true
binlogSaver.On("SaveBinlogPaths", mock.Anything, mock.MatchedBy(func(req *datapb.SaveBinlogPathsRequest) bool {
return req.Flushed == true
})).Return(nil).Once()
err := m.ForceSyncAndSeal(context.Background(), 1001)
assert.NoError(t, err)
binlogSaver.AssertExpectations(t)
})
}
func TestGrowingFlushManager_DoSync_isSeal(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
}
t.Run("isSeal true produces Flushed true", func(t *testing.T) {
tracker := NewCheckpointTracker()
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
binlogSaver := &MockBinlogSaver{}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
binlogSaver,
mockCM,
tracker,
)
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(2001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
seg.EXPECT().RowNum().Return(int64(50)).Maybe()
// Set up tracker: flushed offset = 0, record batch up to 50
pos := &msgpb.MsgPosition{ChannelName: "test-channel", MsgID: []byte{1}, Timestamp: 200}
tracker.RecordBatch(2001, 50, pos)
flushResult := &FlushResult{
ManifestPath: "/test/manifest_seal.json",
NumRows: 50,
}
seg.EXPECT().FlushData(mock.Anything, int64(0), int64(50), mock.Anything).Return(flushResult, nil).Once()
binlogSaver.On("SaveBinlogPaths", mock.Anything, mock.MatchedBy(func(req *datapb.SaveBinlogPathsRequest) bool {
return req.Flushed == true
})).Return(nil).Once()
err := m.doSync(context.Background(), seg, true)
assert.NoError(t, err)
binlogSaver.AssertExpectations(t)
})
t.Run("isSeal false produces Flushed false", func(t *testing.T) {
tracker := NewCheckpointTracker()
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
binlogSaver := &MockBinlogSaver{}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
binlogSaver,
mockCM,
tracker,
)
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(3001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
seg.EXPECT().RowNum().Return(int64(80)).Maybe()
// Set up tracker: flushed offset = 0, record batch up to 80
pos := &msgpb.MsgPosition{ChannelName: "test-channel", MsgID: []byte{1}, Timestamp: 300}
tracker.RecordBatch(3001, 80, pos)
flushResult := &FlushResult{
ManifestPath: "/test/manifest_incr.json",
NumRows: 80,
}
seg.EXPECT().FlushData(mock.Anything, int64(0), int64(80), mock.Anything).Return(flushResult, nil).Once()
binlogSaver.On("SaveBinlogPaths", mock.Anything, mock.MatchedBy(func(req *datapb.SaveBinlogPathsRequest) bool {
return req.Flushed == false
})).Return(nil).Once()
err := m.doSync(context.Background(), seg, false)
assert.NoError(t, err)
binlogSaver.AssertExpectations(t)
})
}
func TestGrowingFlushManager_DoSync_ChannelEmpty(t *testing.T) {
paramtable.Init()
schema := &schemapb.CollectionSchema{
Name: "test_collection",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
}
tracker := NewCheckpointTracker()
mockCM := mock_storage.NewMockChunkManager(t)
mockCM.EXPECT().RootPath().Return("/test/root").Maybe()
binlogSaver := &MockBinlogSaver{}
m := NewGrowingFlushManager(
1,
"test-channel",
schema,
nil,
nil,
binlogSaver,
mockCM,
tracker,
)
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(4001)).Maybe()
seg.EXPECT().Partition().Return(int64(10)).Maybe()
seg.EXPECT().RowNum().Return(int64(60)).Maybe()
// Set up tracker: flushed offset = 0, record batch up to 60
pos := &msgpb.MsgPosition{ChannelName: "test-channel", MsgID: []byte{1}, Timestamp: 400}
tracker.RecordBatch(4001, 60, pos)
flushResult := &FlushResult{
ManifestPath: "/test/manifest_channel.json",
NumRows: 60,
}
seg.EXPECT().FlushData(mock.Anything, int64(0), int64(60), mock.Anything).Return(flushResult, nil).Once()
// Assert that Channel field is empty string in SaveBinlogPaths request
binlogSaver.On("SaveBinlogPaths", mock.Anything, mock.MatchedBy(func(req *datapb.SaveBinlogPathsRequest) bool {
return req.Channel == ""
})).Return(nil).Once()
err := m.doSync(context.Background(), seg, false)
assert.NoError(t, err)
binlogSaver.AssertExpectations(t)
}
@@ -1,307 +0,0 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package segments
import (
"container/heap"
"math/rand"
"sort"
"time"
"github.com/samber/lo"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
// GrowingSegmentBuffer wraps a Growing Segment to provide the interface needed
// for sync policy selection. this is an adapter that allows reusing the sync
// policy algorithms from flushcommon/writebuffer with Growing Segments.
//
// note: we can't directly use writebuffer.SyncPolicy because it expects
// *writebuffer.segmentBuffer which is a private type. instead, we define
// our own GrowingSyncPolicy interface with the same algorithm logic.
type GrowingSegmentBuffer struct {
segment Segment
checkpointTracker *CheckpointTracker
}
// NewGrowingSegmentBuffer creates a new GrowingSegmentBuffer wrapping the given segment.
func NewGrowingSegmentBuffer(segment Segment, tracker *CheckpointTracker) *GrowingSegmentBuffer {
return &GrowingSegmentBuffer{
segment: segment,
checkpointTracker: tracker,
}
}
// SegmentID returns the segment ID.
func (b *GrowingSegmentBuffer) SegmentID() int64 {
return b.segment.ID()
}
// Segment returns the underlying segment.
func (b *GrowingSegmentBuffer) Segment() Segment {
return b.segment
}
// IsFull returns true if the segment's memory usage exceeds the threshold.
// the threshold is configured via paramtable.
func (b *GrowingSegmentBuffer) IsFull() bool {
threshold := paramtable.Get().DataNodeCfg.FlushInsertBufferSize.GetAsInt64()
if threshold <= 0 {
threshold = 16 * 1024 * 1024 // 16MB default
}
return b.segment.MemSize() >= threshold
}
// MinTimestamp returns the minimum timestamp of unflushed data.
func (b *GrowingSegmentBuffer) MinTimestamp() typeutil.Timestamp {
if b.checkpointTracker == nil {
return 0
}
return b.checkpointTracker.GetMinTimestamp(b.segment.ID())
}
// MemorySize returns the current memory usage of the segment.
func (b *GrowingSegmentBuffer) MemorySize() int64 {
return b.segment.MemSize()
}
// RowCount returns the current row count of the segment.
func (b *GrowingSegmentBuffer) RowCount() int64 {
return b.segment.RowNum()
}
// FlushedOffset returns the last flushed row offset.
func (b *GrowingSegmentBuffer) FlushedOffset() int64 {
if b.checkpointTracker == nil {
return 0
}
return b.checkpointTracker.GetFlushedOffset(b.segment.ID())
}
// UnflushedRowCount returns the number of rows that haven't been flushed yet.
func (b *GrowingSegmentBuffer) UnflushedRowCount() int64 {
return b.RowCount() - b.FlushedOffset()
}
// HasUnflushedData returns true if there are unflushed rows.
func (b *GrowingSegmentBuffer) HasUnflushedData() bool {
return b.UnflushedRowCount() > 0
}
// ============================================================================
// GrowingSyncPolicy - sync policy interface for Growing Segments
// ============================================================================
// GrowingSyncPolicy defines the interface for selecting which Growing Segments
// should be synced. this mirrors writebuffer.SyncPolicy but works with
// GrowingSegmentBuffer instead of the private segmentBuffer type.
type GrowingSyncPolicy interface {
// SelectSegments returns the IDs of segments that should be synced.
SelectSegments(buffers []*GrowingSegmentBuffer, ts typeutil.Timestamp) []int64
// Reason returns a human-readable reason for this policy.
Reason() string
}
// GrowingSyncPolicyFunc is a function type that implements GrowingSyncPolicy.
type GrowingSyncPolicyFunc struct {
fn func(buffers []*GrowingSegmentBuffer, ts typeutil.Timestamp) []int64
reason string
}
// SelectSegments implements GrowingSyncPolicy.
func (f GrowingSyncPolicyFunc) SelectSegments(buffers []*GrowingSegmentBuffer, ts typeutil.Timestamp) []int64 {
return f.fn(buffers, ts)
}
// Reason implements GrowingSyncPolicy.
func (f GrowingSyncPolicyFunc) Reason() string {
return f.reason
}
// ============================================================================
// sync policy factory functions
// these mirror the policies in flushcommon/writebuffer/sync_policy.go
// ============================================================================
// GetGrowingFullBufferPolicy returns a policy that selects segments whose buffer is full.
// this mirrors writebuffer.GetFullBufferPolicy.
func GetGrowingFullBufferPolicy() GrowingSyncPolicy {
return GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, _ typeutil.Timestamp) []int64 {
return lo.FilterMap(buffers, func(buf *GrowingSegmentBuffer, _ int) (int64, bool) {
return buf.SegmentID(), buf.IsFull()
})
},
reason: "buffer full",
}
}
// GetGrowingStaleBufferPolicy returns a policy that selects segments with stale data.
// this mirrors writebuffer.GetSyncStaleBufferPolicy.
func GetGrowingStaleBufferPolicy(staleDuration time.Duration) GrowingSyncPolicy {
return GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, ts typeutil.Timestamp) []int64 {
current := tsoutil.PhysicalTime(ts)
return lo.FilterMap(buffers, func(buf *GrowingSegmentBuffer, _ int) (int64, bool) {
minTs := buf.MinTimestamp()
if minTs == 0 {
return 0, false // no unflushed data
}
start := tsoutil.PhysicalTime(minTs)
// add jitter to avoid thundering herd
jitter := time.Duration(rand.Float64() * 0.1 * float64(staleDuration))
return buf.SegmentID(), current.Sub(start) > staleDuration+jitter
})
},
reason: "buffer stale",
}
}
// GetGrowingOldestBufferPolicy returns a policy that selects the N oldest buffers.
// this mirrors writebuffer.GetOldestBufferPolicy.
func GetGrowingOldestBufferPolicy(num int) GrowingSyncPolicy {
return GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, _ typeutil.Timestamp) []int64 {
if len(buffers) <= num {
// return all segments with unflushed data
return lo.FilterMap(buffers, func(buf *GrowingSegmentBuffer, _ int) (int64, bool) {
return buf.SegmentID(), buf.HasUnflushedData()
})
}
// use a max-heap to find the N oldest buffers
h := &growingSegmentHeap{}
heap.Init(h)
for _, buf := range buffers {
if !buf.HasUnflushedData() {
continue
}
heap.Push(h, buf)
if h.Len() > num {
heap.Pop(h)
}
}
return lo.Map(*h, func(buf *GrowingSegmentBuffer, _ int) int64 {
return buf.SegmentID()
})
},
reason: "oldest buffers",
}
}
// GetGrowingMemoryPressurePolicy returns a policy that syncs when total memory exceeds threshold.
func GetGrowingMemoryPressurePolicy(memoryThreshold int64) GrowingSyncPolicy {
return GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, _ typeutil.Timestamp) []int64 {
// calculate total unflushed memory
var totalMemory int64
for _, buf := range buffers {
if buf.HasUnflushedData() {
totalMemory += buf.MemorySize()
}
}
if totalMemory < memoryThreshold {
return nil
}
// memory pressure: sync the largest segments first
// use a max-heap by memory size
type memSegment struct {
id int64
memSize int64
}
segments := make([]memSegment, 0)
for _, buf := range buffers {
if buf.HasUnflushedData() {
segments = append(segments, memSegment{
id: buf.SegmentID(),
memSize: buf.MemorySize(),
})
}
}
// sort by memory size descending
sort.Slice(segments, func(i, j int) bool {
return segments[i].memSize > segments[j].memSize
})
// select segments until we've freed enough memory
var freedMemory int64
targetFree := totalMemory - memoryThreshold/2 // try to get to 50% of threshold
result := make([]int64, 0)
for _, seg := range segments {
if freedMemory >= targetFree {
break
}
result = append(result, seg.id)
freedMemory += seg.memSize
}
return result
},
reason: "memory pressure",
}
}
// GetGrowingRowCountPolicy returns a policy that syncs segments with more than N unflushed rows.
func GetGrowingRowCountPolicy(rowThreshold int64) GrowingSyncPolicy {
return GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, _ typeutil.Timestamp) []int64 {
return lo.FilterMap(buffers, func(buf *GrowingSegmentBuffer, _ int) (int64, bool) {
return buf.SegmentID(), buf.UnflushedRowCount() >= rowThreshold
})
},
reason: "row count threshold",
}
}
// ============================================================================
// heap implementation for oldest buffer policy
// ============================================================================
// growingSegmentHeap implements heap.Interface for selecting oldest segments.
// it's a max-heap based on MinTimestamp (larger timestamp = newer = pop first).
type growingSegmentHeap []*GrowingSegmentBuffer
func (h growingSegmentHeap) Len() int { return len(h) }
func (h growingSegmentHeap) Less(i, j int) bool {
// max-heap: larger timestamp comes first (gets popped first)
return h[i].MinTimestamp() > h[j].MinTimestamp()
}
func (h growingSegmentHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *growingSegmentHeap) Push(x any) {
*h = append(*h, x.(*GrowingSegmentBuffer))
}
func (h *growingSegmentHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
@@ -1,327 +0,0 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package segments
import (
"container/heap"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
)
func init() {
paramtable.Init()
}
func TestGrowingSegmentBuffer_Basic(t *testing.T) {
// Create mock segment
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(int64(1001))
seg.EXPECT().MemSize().Return(int64(1024 * 1024)) // 1MB
seg.EXPECT().RowNum().Return(int64(100))
// Create buffer without checkpoint tracker
buf := NewGrowingSegmentBuffer(seg, nil)
assert.Equal(t, int64(1001), buf.SegmentID())
assert.Equal(t, seg, buf.Segment())
assert.Equal(t, int64(1024*1024), buf.MemorySize())
assert.Equal(t, int64(100), buf.RowCount())
assert.Equal(t, uint64(0), buf.MinTimestamp()) // nil tracker returns 0
assert.Equal(t, int64(0), buf.FlushedOffset()) // nil tracker returns 0
assert.Equal(t, int64(100), buf.UnflushedRowCount())
assert.True(t, buf.HasUnflushedData())
}
func TestGrowingSegmentBuffer_WithTracker(t *testing.T) {
tracker := NewCheckpointTracker()
segID := int64(1001)
// Record some batches
pos1 := &msgpb.MsgPosition{ChannelName: "ch1", MsgID: []byte{1}, Timestamp: 100}
pos2 := &msgpb.MsgPosition{ChannelName: "ch1", MsgID: []byte{2}, Timestamp: 200}
tracker.RecordBatch(segID, 50, pos1)
tracker.RecordBatch(segID, 100, pos2)
// Create mock segment
seg := NewMockSegment(t)
seg.EXPECT().ID().Return(segID).Maybe()
seg.EXPECT().MemSize().Return(int64(1024 * 1024)).Maybe()
seg.EXPECT().RowNum().Return(int64(100)).Maybe()
buf := NewGrowingSegmentBuffer(seg, tracker)
// Initially no flushed data
assert.Equal(t, uint64(100), buf.MinTimestamp()) // timestamp from first batch
assert.Equal(t, int64(0), buf.FlushedOffset())
assert.Equal(t, int64(100), buf.UnflushedRowCount())
assert.True(t, buf.HasUnflushedData())
// After flushing to offset 50
tracker.UpdateFlushedOffset(segID, 50)
assert.Equal(t, uint64(200), buf.MinTimestamp()) // timestamp from second batch
assert.Equal(t, int64(50), buf.FlushedOffset())
assert.Equal(t, int64(50), buf.UnflushedRowCount())
assert.True(t, buf.HasUnflushedData())
// After flushing all
tracker.UpdateFlushedOffset(segID, 100)
assert.Equal(t, uint64(0), buf.MinTimestamp()) // no more unflushed data
assert.Equal(t, int64(100), buf.FlushedOffset())
assert.Equal(t, int64(0), buf.UnflushedRowCount())
assert.False(t, buf.HasUnflushedData())
}
func TestGrowingSegmentBuffer_IsFull(t *testing.T) {
// Get the configured threshold - uses FlushInsertBufferSize (same as IsFull method)
threshold := paramtable.Get().DataNodeCfg.FlushInsertBufferSize.GetAsInt64()
if threshold <= 0 {
threshold = 16 * 1024 * 1024 // 16MB default
}
t.Run("not full", func(t *testing.T) {
seg := NewMockSegment(t)
seg.EXPECT().MemSize().Return(threshold - 1)
buf := NewGrowingSegmentBuffer(seg, nil)
assert.False(t, buf.IsFull())
})
t.Run("full", func(t *testing.T) {
seg := NewMockSegment(t)
seg.EXPECT().MemSize().Return(threshold)
buf := NewGrowingSegmentBuffer(seg, nil)
assert.True(t, buf.IsFull())
})
t.Run("over full", func(t *testing.T) {
seg := NewMockSegment(t)
seg.EXPECT().MemSize().Return(threshold + 1)
buf := NewGrowingSegmentBuffer(seg, nil)
assert.True(t, buf.IsFull())
})
}
// ============================================================================
// Sync Policy Tests
// ============================================================================
func createMockBuffer(id int64, memSize int64, rowCount int64, tracker *CheckpointTracker) *GrowingSegmentBuffer {
seg := &MockSegment{}
seg.On("ID").Return(id)
seg.On("MemSize").Return(memSize)
seg.On("RowNum").Return(rowCount)
return NewGrowingSegmentBuffer(seg, tracker)
}
func TestGetGrowingFullBufferPolicy(t *testing.T) {
threshold := paramtable.Get().DataNodeCfg.FlushInsertBufferSize.GetAsInt64()
if threshold <= 0 {
threshold = 16 * 1024 * 1024
}
policy := GetGrowingFullBufferPolicy()
assert.Equal(t, "buffer full", policy.Reason())
buffers := []*GrowingSegmentBuffer{
createMockBuffer(1, threshold-1, 100, nil), // not full
createMockBuffer(2, threshold, 100, nil), // full
createMockBuffer(3, threshold+1, 100, nil), // full
}
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 2)
assert.Contains(t, selected, int64(2))
assert.Contains(t, selected, int64(3))
}
func TestGetGrowingRowCountPolicy(t *testing.T) {
tracker := NewCheckpointTracker()
// Create buffers with different unflushed row counts
buffers := []*GrowingSegmentBuffer{
createMockBuffer(1, 1024, 50, tracker), // 50 unflushed
createMockBuffer(2, 1024, 100, tracker), // 100 unflushed
createMockBuffer(3, 1024, 150, tracker), // 150 unflushed
}
policy := GetGrowingRowCountPolicy(100)
assert.Equal(t, "row count threshold", policy.Reason())
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 2)
assert.Contains(t, selected, int64(2)) // 100 >= 100
assert.Contains(t, selected, int64(3)) // 150 >= 100
}
func TestGetGrowingStaleBufferPolicy(t *testing.T) {
tracker := NewCheckpointTracker()
staleDuration := 10 * time.Second
// Record batches with different timestamps
now := time.Now()
oldTs := tsoutil.ComposeTSByTime(now.Add(-20*time.Second), 0) // 20s ago - stale
recentTs := tsoutil.ComposeTSByTime(now.Add(-5*time.Second), 0) // 5s ago - not stale
currentTs := tsoutil.ComposeTSByTime(now, 0)
tracker.RecordBatch(1, 100, &msgpb.MsgPosition{Timestamp: oldTs})
tracker.RecordBatch(2, 100, &msgpb.MsgPosition{Timestamp: recentTs})
buffers := []*GrowingSegmentBuffer{
createMockBuffer(1, 1024, 100, tracker), // stale
createMockBuffer(2, 1024, 100, tracker), // not stale
createMockBuffer(3, 1024, 100, tracker), // no data (minTs = 0)
}
policy := GetGrowingStaleBufferPolicy(staleDuration)
assert.Equal(t, "buffer stale", policy.Reason())
// Use current time as the timestamp for comparison
selected := policy.SelectSegments(buffers, currentTs)
// Only segment 1 should be stale (20s > 10s + jitter)
// Segment 3 has no unflushed data (minTs = 0)
assert.Contains(t, selected, int64(1))
assert.NotContains(t, selected, int64(3)) // no unflushed data
}
func TestGetGrowingOldestBufferPolicy(t *testing.T) {
tracker := NewCheckpointTracker()
// Record batches with different timestamps
tracker.RecordBatch(1, 100, &msgpb.MsgPosition{Timestamp: 100})
tracker.RecordBatch(2, 100, &msgpb.MsgPosition{Timestamp: 200})
tracker.RecordBatch(3, 100, &msgpb.MsgPosition{Timestamp: 300})
tracker.RecordBatch(4, 100, &msgpb.MsgPosition{Timestamp: 400})
buffers := []*GrowingSegmentBuffer{
createMockBuffer(1, 1024, 100, tracker), // oldest (ts=100)
createMockBuffer(2, 1024, 100, tracker), // second oldest (ts=200)
createMockBuffer(3, 1024, 100, tracker), // third oldest (ts=300)
createMockBuffer(4, 1024, 100, tracker), // newest (ts=400)
}
t.Run("select 2 oldest", func(t *testing.T) {
policy := GetGrowingOldestBufferPolicy(2)
assert.Equal(t, "oldest buffers", policy.Reason())
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 2)
assert.Contains(t, selected, int64(1))
assert.Contains(t, selected, int64(2))
})
t.Run("select more than available", func(t *testing.T) {
policy := GetGrowingOldestBufferPolicy(10)
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 4) // returns all with unflushed data
})
t.Run("no unflushed data", func(t *testing.T) {
// Flush all data
tracker.UpdateFlushedOffset(1, 100)
tracker.UpdateFlushedOffset(2, 100)
tracker.UpdateFlushedOffset(3, 100)
tracker.UpdateFlushedOffset(4, 100)
policy := GetGrowingOldestBufferPolicy(2)
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 0)
})
}
func TestGetGrowingMemoryPressurePolicy(t *testing.T) {
tracker := NewCheckpointTracker()
// Create buffers with different memory sizes
buffers := []*GrowingSegmentBuffer{
createMockBuffer(1, 10*1024*1024, 100, tracker), // 10MB
createMockBuffer(2, 20*1024*1024, 100, tracker), // 20MB
createMockBuffer(3, 30*1024*1024, 100, tracker), // 30MB
createMockBuffer(4, 40*1024*1024, 100, tracker), // 40MB
}
t.Run("below threshold", func(t *testing.T) {
// Total: 100MB, threshold: 200MB -> no pressure
policy := GetGrowingMemoryPressurePolicy(200 * 1024 * 1024)
assert.Equal(t, "memory pressure", policy.Reason())
selected := policy.SelectSegments(buffers, 0)
assert.Len(t, selected, 0)
})
t.Run("above threshold", func(t *testing.T) {
// Total: 100MB, threshold: 50MB -> pressure
// Target free: 100MB - 25MB = 75MB
policy := GetGrowingMemoryPressurePolicy(50 * 1024 * 1024)
selected := policy.SelectSegments(buffers, 0)
assert.True(t, len(selected) > 0)
// Should select largest segments first (4, 3, ...)
assert.Contains(t, selected, int64(4)) // 40MB - largest
})
}
func TestGrowingSegmentHeap(t *testing.T) {
tracker := NewCheckpointTracker()
// Record batches with different timestamps
tracker.RecordBatch(1, 100, &msgpb.MsgPosition{Timestamp: 300}) // newer
tracker.RecordBatch(2, 100, &msgpb.MsgPosition{Timestamp: 100}) // oldest
tracker.RecordBatch(3, 100, &msgpb.MsgPosition{Timestamp: 200}) // middle
buf1 := createMockBuffer(1, 1024, 100, tracker)
buf2 := createMockBuffer(2, 1024, 100, tracker)
buf3 := createMockBuffer(3, 1024, 100, tracker)
h := &growingSegmentHeap{}
heap.Init(h)
// Push all buffers using heap.Push to maintain heap property
heap.Push(h, buf1)
heap.Push(h, buf2)
heap.Push(h, buf3)
assert.Equal(t, 3, h.Len())
// Pop should return newest first (max-heap by timestamp)
popped := heap.Pop(h).(*GrowingSegmentBuffer)
assert.Equal(t, int64(1), popped.SegmentID()) // ts=300, newest
popped = heap.Pop(h).(*GrowingSegmentBuffer)
assert.Equal(t, int64(3), popped.SegmentID()) // ts=200, middle
popped = heap.Pop(h).(*GrowingSegmentBuffer)
assert.Equal(t, int64(2), popped.SegmentID()) // ts=100, oldest
}
func TestGrowingSyncPolicyFunc(t *testing.T) {
// Test that GrowingSyncPolicyFunc correctly implements the interface
customPolicy := GrowingSyncPolicyFunc{
fn: func(buffers []*GrowingSegmentBuffer, ts uint64) []int64 {
return []int64{1, 2, 3}
},
reason: "custom reason",
}
var policy GrowingSyncPolicy = customPolicy
assert.Equal(t, "custom reason", policy.Reason())
assert.Equal(t, []int64{1, 2, 3}, policy.SelectSegments(nil, 0))
}
+37
View File
@@ -113,6 +113,12 @@ type SegmentManager interface {
RemoveBy(ctx context.Context, filters ...SegmentFilter) (int, int)
Clear(ctx context.Context)
// ReleaseDetached completes the release of a segment previously taken out of
// the active maps via DetachStreaming. It runs the same bookkeeping as the
// normal release path (release callback, segment teardown, metric Dec and
// clearing the on-releasing set).
ReleaseDetached(ctx context.Context, segment Segment)
// Deprecated: quick fix critical issue: #30857
// TODO: All Segment assigned to querynode should be managed by SegmentManager, including loading or releasing to perform a transaction.
Exist(segmentID typeutil.UniqueID, typ SegmentType) bool
@@ -657,6 +663,37 @@ func (mgr *segmentManager) Remove(ctx context.Context, segmentID typeutil.Unique
return removeGrowing, removeSealed
}
// DetachStreaming removes the given growing segment from the active maps
// WITHOUT releasing it. The segment is staged into the on-releasing set so
// Exist() keeps reporting it while growing-source flush handoff keeps the
// segment alive. The owner MUST call ReleaseDetached once it is done,
// otherwise the on-releasing set entry, the segment gauge and the release
// callback are never reconciled.
func (mgr *segmentManager) DetachStreaming(ctx context.Context, segmentID typeutil.UniqueID) int {
removeGrowing := 0
if mgr.removeSegmentWithType(SegmentTypeGrowing, segmentID) != nil {
removeGrowing = 1
}
log.Ctx(ctx).Info("detached segment from active segment manager",
zap.Int64("segmentID", segmentID),
zap.Int("growingCount", removeGrowing))
return removeGrowing
}
// ReleaseDetached completes the release of a segment previously taken out of the
// active maps via DetachStreaming. DetachStreaming deliberately skips release()
// so an out-of-band owner (growing-source flush handoff) can keep the segment
// alive; that owner MUST call ReleaseDetached when it is done. Otherwise the
// on-releasing set entry, the segment gauge (QueryNodeNumSegments) and the
// release callback are never reconciled, and Exist() would keep returning true
// for the segmentID.
func (mgr *segmentManager) ReleaseDetached(ctx context.Context, segment Segment) {
if segment == nil {
return
}
mgr.release(ctx, segment)
}
func (mgr *segmentManager) removeSegmentWithType(typ SegmentType, segmentID typeutil.UniqueID) Segment {
segment, ok := mgr.globalSegments.RemoveWithType(segmentID, typ)
if !ok {
@@ -811,6 +811,98 @@ func (_c *MockSegmentManager_Remove_Call) RunAndReturn(run func(context.Context,
return _c
}
// Detach provides a mock function with given fields: ctx, segmentID, scope
func (_m *MockSegmentManager) Detach(ctx context.Context, segmentID int64, scope querypb.DataScope) (int, int) {
ret := _m.Called(ctx, segmentID, scope)
if len(ret) == 0 {
panic("no return value specified for Detach")
}
var r0 int
var r1 int
if rf, ok := ret.Get(0).(func(context.Context, int64, querypb.DataScope) (int, int)); ok {
return rf(ctx, segmentID, scope)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, querypb.DataScope) int); ok {
r0 = rf(ctx, segmentID, scope)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func(context.Context, int64, querypb.DataScope) int); ok {
r1 = rf(ctx, segmentID, scope)
} else {
r1 = ret.Get(1).(int)
}
return r0, r1
}
// MockSegmentManager_Detach_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Detach'
type MockSegmentManager_Detach_Call struct {
*mock.Call
}
// Detach is a helper method to define mock.On call
// - ctx context.Context
// - segmentID int64
// - scope querypb.DataScope
func (_e *MockSegmentManager_Expecter) Detach(ctx interface{}, segmentID interface{}, scope interface{}) *MockSegmentManager_Detach_Call {
return &MockSegmentManager_Detach_Call{Call: _e.mock.On("Detach", ctx, segmentID, scope)}
}
func (_c *MockSegmentManager_Detach_Call) Run(run func(ctx context.Context, segmentID int64, scope querypb.DataScope)) *MockSegmentManager_Detach_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int64), args[2].(querypb.DataScope))
})
return _c
}
func (_c *MockSegmentManager_Detach_Call) Return(_a0 int, _a1 int) *MockSegmentManager_Detach_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockSegmentManager_Detach_Call) RunAndReturn(run func(context.Context, int64, querypb.DataScope) (int, int)) *MockSegmentManager_Detach_Call {
_c.Call.Return(run)
return _c
}
// ReleaseDetached provides a mock function with given fields: ctx, segment
func (_m *MockSegmentManager) ReleaseDetached(ctx context.Context, segment Segment) {
_m.Called(ctx, segment)
}
// MockSegmentManager_ReleaseDetached_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReleaseDetached'
type MockSegmentManager_ReleaseDetached_Call struct {
*mock.Call
}
// ReleaseDetached is a helper method to define mock.On call
// - ctx context.Context
// - segment Segment
func (_e *MockSegmentManager_Expecter) ReleaseDetached(ctx interface{}, segment interface{}) *MockSegmentManager_ReleaseDetached_Call {
return &MockSegmentManager_ReleaseDetached_Call{Call: _e.mock.On("ReleaseDetached", ctx, segment)}
}
func (_c *MockSegmentManager_ReleaseDetached_Call) Run(run func(ctx context.Context, segment Segment)) *MockSegmentManager_ReleaseDetached_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(Segment))
})
return _c
}
func (_c *MockSegmentManager_ReleaseDetached_Call) Return() *MockSegmentManager_ReleaseDetached_Call {
_c.Call.Return()
return _c
}
func (_c *MockSegmentManager_ReleaseDetached_Call) RunAndReturn(run func(context.Context, Segment)) *MockSegmentManager_ReleaseDetached_Call {
_c.Run(run)
return _c
}
// RemoveBy provides a mock function with given fields: ctx, filters
func (_m *MockSegmentManager) RemoveBy(ctx context.Context, filters ...SegmentFilter) (int, int) {
_va := make([]interface{}, len(filters))
@@ -30,6 +30,12 @@ import (
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
// BinlogSaver is a minimal interface for saving binlog paths to DataCoord.
// This avoids depending on the full broker.Broker interface.
type BinlogSaver interface {
SaveBinlogPaths(ctx context.Context, req *datapb.SaveBinlogPathsRequest) error
}
// ResourceUsage is used to estimate the resource usage of a sealed segment.
type ResourceUsage struct {
MemorySize uint64
@@ -443,16 +443,19 @@ func (loader *segmentLoader) prepare(ctx context.Context, segmentType SegmentTyp
// filter out loaded & loading segments
infos := make([]*querypb.SegmentLoadInfo, 0, len(segments))
for _, segment := range segments {
// Not loaded & loading & releasing.
if !loader.manager.Segment.Exist(segment.GetSegmentID(), segmentType) &&
!loader.loadingSegments.Contain(segment.GetSegmentID()) {
// Only active loaded segments should be skipped here. SegmentManager.Exist()
// also reports detached/on-releasing segments, which are no longer active
// and must be allowed to load again.
isLoaded := loader.manager.Segment.GetWithType(segment.GetSegmentID(), segmentType) != nil
isLoading := loader.loadingSegments.Contain(segment.GetSegmentID())
if !isLoaded && !isLoading {
infos = append(infos, segment)
loader.loadingSegments.Insert(segment.GetSegmentID(), newLoadResult())
} else {
log.Info("skip loaded/loading segment",
zap.Int64("segmentID", segment.GetSegmentID()),
zap.Bool("isLoaded", len(loader.manager.Segment.GetBy(WithType(segmentType), WithID(segment.GetSegmentID()))) > 0),
zap.Bool("isLoading", loader.loadingSegments.Contain(segment.GetSegmentID())),
zap.Bool("isLoaded", isLoaded),
zap.Bool("isLoading", isLoading),
)
}
}
@@ -46,6 +46,7 @@ import (
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/metric"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
type SegmentLoaderSuite struct {
@@ -1054,26 +1055,26 @@ func (suite *SegmentLoaderDetailSuite) TestWaitSegmentLoadDone() {
suite.Run("wait_success", func() {
idx := 0
var infos []*querypb.SegmentLoadInfo
suite.segmentManager.EXPECT().Exist(mock.Anything, mock.Anything).Return(false)
suite.segmentManager.EXPECT().GetWithType(suite.segmentID, SegmentTypeSealed).RunAndReturn(func(segmentID int64, segmentType commonpb.SegmentState) Segment {
defer func() { idx++ }()
if idx == 0 {
go func() {
<-time.After(time.Second)
suite.loader.notifyLoadFinish(infos...)
}()
}
return nil
})
suite.segmentManager.EXPECT().UpdateBy(mock.Anything, mock.Anything, mock.Anything).Return(0)
infos = suite.loader.prepare(context.Background(), SegmentTypeSealed, &querypb.SegmentLoadInfo{
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 100,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
suite.segmentManager.EXPECT().GetWithType(suite.segmentID, SegmentTypeSealed).RunAndReturn(func(segmentID int64, segmentType commonpb.SegmentState) Segment {
defer func() { idx++ }()
if idx == 0 {
go func() {
<-time.After(time.Second)
suite.loader.notifyLoadFinish(loadInfo)
}()
}
return nil
})
suite.segmentManager.EXPECT().UpdateBy(mock.Anything, mock.Anything, mock.Anything).Return(0)
infos := suite.loader.prepare(context.Background(), SegmentTypeSealed, loadInfo)
suite.Len(infos, 1)
err := suite.loader.waitSegmentLoadDone(context.Background(), SegmentTypeSealed, []int64{suite.segmentID}, 0)
suite.NoError(err)
@@ -1083,26 +1084,26 @@ func (suite *SegmentLoaderDetailSuite) TestWaitSegmentLoadDone() {
suite.SetupTest()
var idx int
var infos []*querypb.SegmentLoadInfo
suite.segmentManager.EXPECT().Exist(mock.Anything, mock.Anything).Return(false)
suite.segmentManager.EXPECT().GetWithType(suite.segmentID, SegmentTypeSealed).RunAndReturn(func(segmentID int64, segmentType commonpb.SegmentState) Segment {
defer func() { idx++ }()
if idx == 0 {
go func() {
<-time.After(time.Second)
suite.loader.unregister(infos...)
}()
}
return nil
})
infos = suite.loader.prepare(context.Background(), SegmentTypeSealed, &querypb.SegmentLoadInfo{
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 100,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
suite.segmentManager.EXPECT().GetWithType(suite.segmentID, SegmentTypeSealed).RunAndReturn(func(segmentID int64, segmentType commonpb.SegmentState) Segment {
defer func() { idx++ }()
if idx == 0 {
go func() {
<-time.After(time.Second)
suite.loader.unregister(loadInfo)
}()
}
return nil
})
infos := suite.loader.prepare(context.Background(), SegmentTypeSealed, loadInfo)
suite.Len(infos, 1)
err := suite.loader.waitSegmentLoadDone(context.Background(), SegmentTypeSealed, []int64{suite.segmentID}, 0)
suite.Error(err)
@@ -1111,7 +1112,6 @@ func (suite *SegmentLoaderDetailSuite) TestWaitSegmentLoadDone() {
suite.Run("wait_timeout", func() {
suite.SetupTest()
suite.segmentManager.EXPECT().Exist(mock.Anything, mock.Anything).Return(false)
suite.segmentManager.EXPECT().GetWithType(suite.segmentID, SegmentTypeSealed).RunAndReturn(func(segmentID int64, segmentType commonpb.SegmentState) Segment {
return nil
})
@@ -1132,6 +1132,32 @@ func (suite *SegmentLoaderDetailSuite) TestWaitSegmentLoadDone() {
})
}
func TestSegmentLoaderPrepareLoadsWhenSegmentIsNotActive(t *testing.T) {
segmentID := rand.Int63()
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: rand.Int63(),
CollectionID: rand.Int63(),
NumOfRows: 100,
InsertChannel: "by-dev-rootcoord-dml_0_1v0",
}
segmentManager := NewMockSegmentManager(t)
loader := &segmentLoader{
manager: &Manager{
Segment: segmentManager,
},
loadingSegments: typeutil.NewConcurrentMap[int64, *loadResult](),
}
segmentManager.EXPECT().GetWithType(segmentID, SegmentTypeGrowing).Return(nil).Once()
infos := loader.prepare(context.Background(), SegmentTypeGrowing, loadInfo)
assert.Len(t, infos, 1)
assert.Equal(t, segmentID, infos[0].GetSegmentID())
assert.True(t, loader.loadingSegments.Contain(segmentID))
}
func TestConfigureUseTakeForOutput(t *testing.T) {
paramtable.Init()
internalKey := paramtable.Get().QueryNodeCfg.InternalCollectionUseTakeForOutput.Key
+2 -2
View File
@@ -147,7 +147,7 @@ type QueryNode struct {
metricsRequest *metricsinfo.MetricsRequest
// binlogSaver for TEXT collection growing segment flush
// binlogSaver for growing-source segment flush
binlogSaver segments.BinlogSaver
}
@@ -569,7 +569,7 @@ func (node *QueryNode) SetEtcdClient(client *clientv3.Client) {
node.etcdCli = client
}
// SetBinlogSaver sets the BinlogSaver for TEXT collection growing segment flush.
// SetBinlogSaver sets the BinlogSaver for growing-source segment flush.
func (node *QueryNode) SetBinlogSaver(saver segments.BinlogSaver) {
node.binlogSaver = saver
}
+93 -4
View File
@@ -23,6 +23,7 @@ import (
"sync"
"time"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
@@ -32,14 +33,19 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus/internal/distributed/streaming"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/querynodev2/delegator"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
"github.com/milvus-io/milvus/internal/querynodev2/tasks"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2"
"github.com/milvus-io/milvus/internal/streamingnode/client/handler"
"github.com/milvus-io/milvus/internal/streamingnode/client/handler/registry"
"github.com/milvus-io/milvus/internal/util/analyzer"
"github.com/milvus-io/milvus/internal/util/fileresource"
"github.com/milvus-io/milvus/internal/util/searchutil/scheduler"
streamingstatus "github.com/milvus-io/milvus/internal/util/streamingutil/status"
"github.com/milvus-io/milvus/internal/util/streamrpc"
"github.com/milvus-io/milvus/internal/util/textmatch"
"github.com/milvus-io/milvus/pkg/v3/common"
@@ -61,6 +67,10 @@ import (
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
type segmentDetacher interface {
DetachStreaming(ctx context.Context, segmentID typeutil.UniqueID) int
}
// GetComponentStates returns information about whether the node is healthy
func (node *QueryNode) GetComponentStates(ctx context.Context, req *milvuspb.GetComponentStatesRequest) (*milvuspb.ComponentStates, error) {
stats := &milvuspb.ComponentStates{
@@ -360,13 +370,52 @@ func (node *QueryNode) UnsubDmChannel(ctx context.Context, req *querypb.UnsubDmC
node.unsubscribingChannels.Insert(req.GetChannelName())
defer node.unsubscribingChannels.Remove(req.GetChannelName())
delegator, ok := node.delegators.GetAndRemove(req.GetChannelName())
_, ok := node.delegators.Get(req.GetChannelName())
if ok {
node.pipelineManager.Remove(req.GetChannelName())
growingSegmentIDs := node.localGrowingSegmentIDs(req.GetChannelName(), nil)
prepared, err := node.prepareReleaseManualFlush(ctx, req.GetCollectionID(), req.GetChannelName(), growingSegmentIDs)
prepareSkipped := false
if err != nil {
if isReleaseManualFlushPrepareUnavailable(err) {
log.Warn("release manual flush prepare unavailable before unsubscribing channel, continue unsubscribe",
zap.Int64s("segmentIDs", growingSegmentIDs),
zap.Error(err))
prepared = false
prepareSkipped = true
} else {
log.Warn("failed to prepare release manual flush before unsubscribing channel",
zap.Int64s("segmentIDs", growingSegmentIDs),
zap.Error(err))
return merr.Status(err), nil
}
}
if prepareSkipped {
log.Info("release manual flush prepare skipped before unsubscribing channel",
zap.Int64s("segmentIDs", growingSegmentIDs),
zap.Bool("prepared", prepared))
} else {
log.Info("release manual flush prepare result before unsubscribing channel",
zap.Int64s("segmentIDs", growingSegmentIDs),
zap.Bool("prepared", prepared))
}
delegator, ok := node.delegators.GetAndRemove(req.GetChannelName())
if !ok {
log.Info("channel already unsubscribed")
return merr.Success(), nil
}
node.pipelineManager.Remove(req.GetChannelName())
preparedGrowingSourceSegments := syncmgr.DefaultGrowingSourceRegistry().ReleasePreparedSegments(req.GetChannelName())
// close the delegator first to block all coming query/search requests
delegator.Close()
if detacher, ok := node.manager.Segment.(segmentDetacher); ok {
for _, segmentID := range preparedGrowingSourceSegments {
detacher.DetachStreaming(ctx, segmentID)
syncmgr.DefaultGrowingSourceRegistry().MarkReleaseDetached(req.GetChannelName(), segmentID)
syncmgr.DefaultGrowingSourceRegistry().ClearReleasePrepared(req.GetChannelName(), segmentID)
}
}
node.manager.Segment.RemoveBy(ctx, segments.WithChannel(req.GetChannelName()), segments.WithType(segments.SegmentTypeGrowing))
node.manager.Collection.Unref(req.GetCollectionID(), 1)
}
@@ -598,7 +647,7 @@ func (node *QueryNode) ReleaseSegments(ctx context.Context, req *querypb.Release
defer node.lifetime.Done()
if req.GetNeedTransfer() {
delegator, ok := node.delegators.Get(req.GetShard())
shardDelegator, ok := node.delegators.Get(req.GetShard())
if !ok {
msg := "failed to release segment, delegator not found"
log.Warn(msg)
@@ -607,7 +656,7 @@ func (node *QueryNode) ReleaseSegments(ctx context.Context, req *querypb.Release
}
req.NeedTransfer = false
err := delegator.ReleaseSegments(ctx, req, false)
err := shardDelegator.ReleaseSegments(ctx, req, false)
if err != nil {
log.Warn("delegator failed to release segment", zap.Error(err))
return merr.Status(err), nil
@@ -627,6 +676,46 @@ func (node *QueryNode) ReleaseSegments(ctx context.Context, req *querypb.Release
return merr.Success(), nil
}
func (node *QueryNode) localGrowingSegmentIDs(channel string, segmentIDs []int64) []int64 {
filters := []segments.SegmentFilter{
segments.WithChannel(channel),
segments.WithType(segments.SegmentTypeGrowing),
}
if len(segmentIDs) > 0 {
filters = append(filters, segments.WithIDs(segmentIDs...))
}
return lo.Map(node.manager.Segment.GetBy(filters...), func(segment segments.Segment, _ int) int64 {
return segment.ID()
})
}
func (node *QueryNode) prepareReleaseManualFlush(ctx context.Context, collectionID int64, channel string, segmentIDs []int64) (bool, error) {
segmentIDs = lo.Uniq(lo.Filter(segmentIDs, func(segmentID int64, _ int) bool {
return segmentID > 0 && !syncmgr.DefaultGrowingSourceRegistry().IsReleasePrepared(channel, segmentID, 0)
}))
if len(segmentIDs) == 0 {
return false, nil
}
wal := streaming.WAL()
if wal == nil {
return false, merr.WrapErrServiceUnavailable("streaming WAL is not initialized")
}
return wal.PrepareReleaseManualFlush(ctx, collectionID, channel, segmentIDs)
}
func isReleaseManualFlushPrepareUnavailable(err error) bool {
if err == nil {
return false
}
if errors.Is(err, merr.ErrServiceUnavailable) ||
errors.Is(err, handler.ErrClientClosed) ||
errors.Is(err, registry.ErrNoStreamingNodeDeployed) ||
errors.Is(err, registry.ErrNoReleaseManualFlushPreparer) {
return true
}
return streamingstatus.AsStreamingError(err).IsOnShutdown()
}
// GetSegmentInfo returns segment information of the collection on the queryNode, and the information includes memSize, numRow, indexName, indexID ...
func (node *QueryNode) GetSegmentInfo(ctx context.Context, in *querypb.GetSegmentInfoRequest) (*querypb.GetSegmentInfoResponse, error) {
if err := node.lifetime.Add(merr.IsHealthy); err != nil {
+59
View File
@@ -28,6 +28,7 @@ import (
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
clientv3 "go.etcd.io/etcd/client/v3"
@@ -45,7 +46,10 @@ import (
"github.com/milvus-io/milvus/internal/querynodev2/delegator"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/streamingnode/client/handler"
"github.com/milvus-io/milvus/internal/streamingnode/client/handler/registry"
"github.com/milvus-io/milvus/internal/util/dependency"
streamingstatus "github.com/milvus-io/milvus/internal/util/streamingutil/status"
"github.com/milvus-io/milvus/internal/util/streamingutil/util"
"github.com/milvus-io/milvus/internal/util/streamrpc"
"github.com/milvus-io/milvus/pkg/v3/common"
@@ -562,6 +566,61 @@ func (suite *ServiceSuite) TestUnsubDmChannels_Failed() {
suite.Equal(commonpb.ErrorCode_NotReadyServe, status.GetErrorCode())
}
func TestIsReleaseManualFlushPrepareUnavailable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "nil",
err: nil,
want: false,
},
{
name: "service unavailable",
err: merr.WrapErrServiceUnavailable("streaming WAL is not initialized"),
want: true,
},
{
name: "handler client closed",
err: handler.ErrClientClosed,
want: true,
},
{
name: "no streaming node deployed",
err: registry.ErrNoStreamingNodeDeployed,
want: true,
},
{
name: "no release manual flush preparer",
err: registry.ErrNoReleaseManualFlushPreparer,
want: true,
},
{
name: "streaming on shutdown",
err: streamingstatus.NewOnShutdownError("wal is on shutdown"),
want: true,
},
{
name: "generic prepare failure",
err: errors.New("prepare failed"),
want: false,
},
{
name: "streaming inner failure",
err: streamingstatus.NewInner("prepare failed"),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isReleaseManualFlushPrepareUnavailable(tt.err))
})
}
}
func (suite *ServiceSuite) genSegmentLoadInfos(schema *schemapb.CollectionSchema,
indexInfos []*indexpb.IndexInfo,
) []*querypb.SegmentLoadInfo {
@@ -198,6 +198,9 @@ func (t *createCollectionTask) validateSchema(ctx context.Context, schema *schem
if err := typeutil.NormalizeAndValidateExternalCollectionSchema(schema); err != nil {
return err
}
if err := typeutil.ValidateTextRequiresStorageV3(schema, Params.CommonCfg.UseLoonFFI.GetAsBool()); err != nil {
return merr.WrapErrParameterInvalidMsg("%s", err.Error())
}
// For external collections, validate the source URL scheme allowlist and
// the JSON spec structure (extfs allowlist + format whitelist) at the
@@ -79,6 +79,9 @@ func (c *Core) broadcastAlterCollectionForAddField(ctx context.Context, req *mil
if err := typeutil.ValidateExternalCollectionResolvedSchema(schema); err != nil {
return err
}
if err := typeutil.ValidateTextRequiresStorageV3(schema, Params.CommonCfg.UseLoonFFI.GetAsBool()); err != nil {
return merr.WrapErrParameterInvalidMsg("%s", err.Error())
}
cacheExpirations, err := c.getCacheExpireForCollection(ctx, req.GetDbName(), req.GetCollectionName())
if err != nil {
@@ -185,6 +185,9 @@ func (c *Core) broadcastAlterCollectionSchemaAdd(ctx context.Context, broadcaste
if err := typeutil.ValidateExternalCollectionResolvedSchema(schema); err != nil {
return err
}
if err := typeutil.ValidateTextRequiresStorageV3(schema, Params.CommonCfg.UseLoonFFI.GetAsBool()); err != nil {
return merr.WrapErrParameterInvalidMsg("%s", err.Error())
}
// Broadcast.
cacheExpirations, err := c.getCacheExpireForCollection(ctx, req.GetDbName(), req.GetCollectionName())
+1
View File
@@ -539,6 +539,7 @@ func NewPackedTextBatchWriter(
config := &packed.SegmentWriterConfig{
SegmentPath: basePath,
TextColumns: textColumnConfigs,
ColumnGroups: columnGroups,
WriterFormat: writerFormat,
SchemaBasedPattern: schemaBasedPattern,
SchemaBasedFormats: schemaBasedFormats,
+1 -1
View File
@@ -61,7 +61,7 @@ var (
PropertyWriterPolicy = C.GoString(C.loon_properties_writer_policy)
PropertyWriterFormat = "writer.format"
PropertyWriterSchemaBasedPattern = C.GoString(C.loon_properties_writer_schema_base_patterns)
PropertyWriterSchemaBasedFormats = C.GoString(C.loon_properties_writer_schema_base_formats)
PropertyWriterSchemaBasedFormats = "writer.split.schema_based.formats"
// CMEK (Customer Managed Encryption Keys) writer properties
PropertyWriterEncEnable = C.GoString(C.loon_properties_writer_enc_enable) // Enable encryption for written data
+41 -12
View File
@@ -31,7 +31,9 @@ import (
"github.com/apache/arrow/go/v17/arrow"
"github.com/apache/arrow/go/v17/arrow/cdata"
"github.com/samber/lo"
"github.com/milvus-io/milvus/internal/storagecommon"
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
)
@@ -51,6 +53,7 @@ type TextColumnConfig struct {
type SegmentWriterConfig struct {
SegmentPath string
TextColumns []TextColumnConfig
ColumnGroups []storagecommon.ColumnGroup
WriterFormat string
SchemaBasedPattern string
SchemaBasedFormats []string
@@ -80,18 +83,7 @@ func NewFFISegmentWriter(
return nil, fmt.Errorf("storageConfig must not be nil")
}
// create properties
extra := map[string]string{}
if config != nil && config.WriterFormat != "" {
extra[PropertyWriterFormat] = config.WriterFormat
}
if config != nil && config.SchemaBasedPattern != "" {
extra[PropertyWriterPolicy] = "schema_based"
extra[PropertyWriterSchemaBasedPattern] = config.SchemaBasedPattern
}
if config != nil && len(config.SchemaBasedFormats) > 0 {
extra[PropertyWriterSchemaBasedFormats] = strings.Join(config.SchemaBasedFormats, ",")
}
extra := segmentWriterProperties(schema, config)
cProperties, err := MakePropertiesFromStorageConfig(storageConfig, extra)
if err != nil {
return nil, err
@@ -116,6 +108,43 @@ func NewFFISegmentWriter(
}, nil
}
func segmentWriterProperties(schema *arrow.Schema, config *SegmentWriterConfig) map[string]string {
extra := map[string]string{
PropertyWriterFormat: "parquet",
}
if config == nil {
extra[PropertyWriterPolicy] = "single"
return extra
}
if config.WriterFormat != "" {
extra[PropertyWriterFormat] = config.WriterFormat
}
if len(config.SchemaBasedFormats) > 0 {
extra[PropertyWriterSchemaBasedFormats] = strings.Join(config.SchemaBasedFormats, ",")
}
if config.SchemaBasedPattern != "" {
extra[PropertyWriterPolicy] = "schema_based"
extra[PropertyWriterSchemaBasedPattern] = config.SchemaBasedPattern
return extra
}
columnGroups := config.ColumnGroups
if len(columnGroups) == 0 {
extra[PropertyWriterPolicy] = "single"
return extra
}
pattern := strings.Join(lo.Map(columnGroups, func(columnGroup storagecommon.ColumnGroup, _ int) string {
return strings.Join(lo.Map(columnGroup.Columns, func(index int, _ int) string {
return schema.Field(index).Name
}), "|")
}), ",")
extra[PropertyWriterPolicy] = "schema_based"
extra[PropertyWriterSchemaBasedPattern] = pattern
return extra
}
// Write writes a record batch to the segment writer.
func (w *FFISegmentWriter) Write(record arrow.Record) error {
var caa cdata.CArrowArray
@@ -85,6 +85,11 @@ type HandlerClient interface {
// Returns an empty slice if no force promote has occurred.
GetSalvageCheckpoint(ctx context.Context, channelName string) ([]*wal.ReplicateCheckpoint, error)
// PrepareReleaseManualFlush prepares process-local release handoff.
// Returns false when the current process is not the local flush owner or
// the channel does not need growing-source retention.
PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error)
// GetWALMetricsIfLocal gets the metrics of the local wal.
// It will only return the metrics of the local wal but not the remote wal.
GetWALMetricsIfLocal(ctx context.Context) (*types.StreamingNodeMetrics, error)
@@ -150,6 +150,54 @@ func (hc *handlerClientImpl) GetSalvageCheckpoint(ctx context.Context, pchannel
return cps.([]*wal.ReplicateCheckpoint), nil
}
// PrepareReleaseManualFlush appends a normal ManualFlush and prepares local growing-source retention.
func (hc *handlerClientImpl) PrepareReleaseManualFlush(ctx context.Context, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
if !hc.lifetime.Add(typeutil.LifetimeStateWorking) {
return false, ErrClientClosed
}
defer hc.lifetime.Done()
pchannel := funcutil.ToPhysicalChannel(vchannel)
logger := log.With(
zap.String("pchannel", pchannel),
zap.String("vchannel", vchannel),
zap.Int64("collectionID", collectionID),
zap.Int64s("releaseSegmentIDs", releaseSegmentIDs),
zap.String("handler", "prepare release manual flush"),
)
result, err := hc.createHandlerAfterStreamingNodeReady(ctx, logger, pchannel, func(ctx context.Context, assign *types.PChannelInfoAssigned) (any, error) {
if assign.Channel.AccessMode != types.AccessModeRW {
logger.Info("skip release manual flush prepare because channel is not RW",
zap.String("accessMode", assign.Channel.AccessMode.String()))
return false, nil
}
_, err := registry.GetLocalAvailableWAL(assign.Channel)
if err != nil {
if errors.Is(err, registry.ErrNoStreamingNodeDeployed) ||
status.AsStreamingError(err).IsWrongStreamingNode() {
logger.Info("skip release manual flush prepare because channel is not owned by local streaming node",
zap.Error(err))
return false, nil
}
return false, err
}
preparer, err := registry.GetLocalReleaseManualFlushPreparer()
if err != nil {
return false, err
}
return preparer.PrepareReleaseManualFlush(ctx, assign.Channel, collectionID, vchannel, releaseSegmentIDs)
})
if err != nil {
return false, err
}
if result == nil {
return false, nil
}
return result.(bool), nil
}
// GetWALMetricsIfLocal gets the metrics of the local wal.
func (hc *handlerClientImpl) GetWALMetricsIfLocal(ctx context.Context) (*types.StreamingNodeMetrics, error) {
if !hc.lifetime.Add(typeutil.LifetimeStateWorking) {
@@ -194,6 +194,40 @@ func TestHandlerClient_GetSalvageCheckpoint(t *testing.T) {
assert.Nil(t, cps)
}
func TestHandlerClient_PrepareReleaseManualFlush(t *testing.T) {
assignment := &types.PChannelInfoAssigned{
Channel: types.PChannelInfo{Name: "pchannel", Term: 1, AccessMode: types.AccessModeRO},
Node: types.StreamingNodeInfo{ServerID: 1, Address: "localhost"},
}
vchannel := "pchannel_100v0"
releaseSegmentIDs := []int64{1001}
service := mock_lazygrpc.NewMockService[streamingpb.StreamingNodeHandlerServiceClient](t)
service.EXPECT().Close().Return()
rb := mock_resolver.NewMockBuilder(t)
rb.EXPECT().Close().Run(func() {})
w := mock_assignment.NewMockWatcher(t)
w.EXPECT().Get(mock.Anything, "pchannel").Return(assignment)
w.EXPECT().Close().Run(func() {})
handler := &handlerClientImpl{
lifetime: typeutil.NewLifetime(),
service: service,
rb: rb,
watcher: w,
}
prepared, err := handler.PrepareReleaseManualFlush(context.Background(), 100, vchannel, releaseSegmentIDs)
assert.NoError(t, err)
assert.False(t, prepared)
handler.Close()
prepared, err = handler.PrepareReleaseManualFlush(context.Background(), 100, vchannel, releaseSegmentIDs)
assert.ErrorIs(t, err, ErrClientClosed)
assert.False(t, prepared)
}
func TestDial(t *testing.T) {
paramtable.Init()
@@ -0,0 +1,43 @@
package registry
import (
"context"
"github.com/cockroachdb/errors"
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/syncutil"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
var (
releaseManualFlushPreparerRegistry = syncutil.NewFuture[ReleaseManualFlushPreparer]()
ErrNoReleaseManualFlushPreparer = errors.New("no release manual flush preparer")
)
// ReleaseManualFlushPreparer prepares process-local release handoff.
type ReleaseManualFlushPreparer interface {
PrepareReleaseManualFlush(ctx context.Context, pchannel types.PChannelInfo, collectionID int64, vchannel string, releaseSegmentIDs []int64) (prepared bool, err error)
}
// RegisterLocalReleaseManualFlushPreparer registers the process-local release handoff preparer.
func RegisterLocalReleaseManualFlushPreparer(preparer ReleaseManualFlushPreparer) {
if !paramtable.IsLocalComponentEnabled(typeutil.StreamingNodeRole) {
panic("unreachable: streaming node is not enabled but release manual flush preparer setup")
}
releaseManualFlushPreparerRegistry.Set(preparer)
log.Ctx(context.Background()).Info("register local release manual flush preparer done")
}
// GetLocalReleaseManualFlushPreparer returns the process-local release handoff preparer.
func GetLocalReleaseManualFlushPreparer() (ReleaseManualFlushPreparer, error) {
if !paramtable.IsLocalComponentEnabled(typeutil.StreamingNodeRole) {
return nil, ErrNoStreamingNodeDeployed
}
if !releaseManualFlushPreparerRegistry.Ready() {
return nil, ErrNoReleaseManualFlushPreparer
}
return releaseManualFlushPreparerRegistry.Get(), nil
}
@@ -7,4 +7,5 @@ import "github.com/milvus-io/milvus/pkg/v3/util/syncutil"
func ResetRegisterLocalWALManager() {
registry = syncutil.NewFuture[WALManager]()
releaseManualFlushPreparerRegistry = syncutil.NewFuture[ReleaseManualFlushPreparer]()
}
@@ -50,14 +50,6 @@ func (impl *msgHandlerImpl) HandleCreateSegment(ctx context.Context, createSegme
}
logger := log.With(log.FieldMessage(createSegmentMsg))
// For TEXT collections, skip creating local metacache entry.
// Insert data is flushed by QueryNode's GrowingFlushManager, not StreamNode.
// This avoids useless empty SyncTasks and manifest path conflicts.
if impl.wbMgr.HasTextFields(vchannel) {
logger.Info("skip CreateNewGrowingSegment for TEXT collection, managed by QueryNode")
return nil
}
if err := impl.wbMgr.CreateNewGrowingSegment(ctx, vchannel, h.PartitionId, h.SegmentId, h.SchemaVersion); err != nil {
logger.Warn("fail to create new growing segment")
return err
@@ -103,12 +103,12 @@ func TestFlushMsgHandler_HandleManualFlush(t *testing.T) {
assert.NoError(t, err)
}
func TestFlushMsgHandler_HandleCreateSegment_TextSkip(t *testing.T) {
func TestFlushMsgHandler_HandleCreateSegment(t *testing.T) {
vchannel := "ch-0"
// Build a CreateSegment message with L0 level so that createNewGrowingSegment
// returns nil immediately (L0 skips MixCoordClient allocation).
// This lets us test the TEXT skip logic in HandleCreateSegment without
// This lets us test the growing-source skip logic in HandleCreateSegment without
// requiring the full resource.Resource() server context.
msg, err := message.NewCreateSegmentMessageBuilderV2().
WithHeader(&message.CreateSegmentMessageHeader{
@@ -130,20 +130,17 @@ func TestFlushMsgHandler_HandleCreateSegment_TextSkip(t *testing.T) {
)
assert.NoError(t, err)
t.Run("TEXT collection skips CreateNewGrowingSegment", func(t *testing.T) {
t.Run("growing-source collection creates WriteBuffer shell", func(t *testing.T) {
wbMgr := writebuffer.NewMockBufferManager(t)
wbMgr.EXPECT().HasTextFields(vchannel).Return(true)
// CreateNewGrowingSegment should NOT be called — no expectation set.
// If called, testify will fail the test.
wbMgr.EXPECT().CreateNewGrowingSegment(mock.Anything, vchannel, int64(10), int64(1001), mock.Anything).Return(nil)
handler := newMsgHandler(wbMgr)
err := handler.HandleCreateSegment(context.Background(), im)
assert.NoError(t, err)
})
t.Run("non-TEXT collection calls CreateNewGrowingSegment", func(t *testing.T) {
t.Run("non-growing-source collection calls CreateNewGrowingSegment", func(t *testing.T) {
wbMgr := writebuffer.NewMockBufferManager(t)
wbMgr.EXPECT().HasTextFields(vchannel).Return(false)
wbMgr.EXPECT().CreateNewGrowingSegment(mock.Anything, vchannel, int64(10), int64(1001), mock.Anything).Return(nil)
handler := newMsgHandler(wbMgr)
+2
View File
@@ -78,6 +78,8 @@ func (s *Server) initBasicComponent() {
// initService initializes the grpc service.
func (s *Server) initService() {
writeBufferManager := resource.Resource().WriteBufferManager()
registry.RegisterLocalReleaseManualFlushPreparer(service.NewReleaseManualFlushPreparer(s.walManager, writeBufferManager))
s.handlerService = service.NewHandlerService(s.walManager)
s.managerService = service.NewManagerService(s.walManager)
s.registerGRPCService(s.grpcServer)
@@ -0,0 +1,133 @@
package service
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/protobuf/types/known/anypb"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/writebuffer"
"github.com/milvus-io/milvus/internal/mocks/streamingnode/server/mock_wal"
"github.com/milvus-io/milvus/internal/mocks/streamingnode/server/mock_walmanager"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/message"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
)
type releaseManualFlushCheckBufferManager struct {
*writebuffer.MockBufferManager
needManualFlush bool
err error
}
func (m *releaseManualFlushCheckBufferManager) CheckReleaseManualFlushNeed(ctx context.Context, channel string, segmentIDs []int64) (bool, error) {
return m.needManualFlush, m.err
}
func TestReleaseManualFlushPreparer(t *testing.T) {
ctx := context.Background()
releaseSegmentIDs := []int64{1001}
affectedSegmentIDs := []int64{1002}
extra, err := anypb.New(&message.ManualFlushExtraResponse{SegmentIds: affectedSegmentIDs})
assert.NoError(t, err)
wal := mock_wal.NewMockWAL(t)
wal.EXPECT().Append(mock.Anything, mock.MatchedBy(func(msg message.MutableMessage) bool {
flushMsg, err := message.AsMutableManualFlushMessageV2(msg)
if err != nil {
return false
}
return flushMsg.VChannel() == "vchannel" &&
flushMsg.Header().GetCollectionId() == 10
})).Return(&types.AppendResult{
TimeTick: 200,
Extra: extra,
}, nil)
manager := mock_walmanager.NewMockManager(t)
manager.EXPECT().GetAvailableWAL(mock.Anything).Return(wal, nil)
wbManager := writebuffer.NewMockBufferManager(t)
wbManager.EXPECT().UseGrowingSourceFlush("vchannel").Return(true)
wbManager.EXPECT().
GetGrowingFlushProgress(mock.Anything, "vchannel", releaseSegmentIDs, uint64(200)).
Return([]writebuffer.GrowingFlushSegmentProgress{
{
SegmentID: 1001,
TargetOffset: 10,
NeedReleaseHandoff: true,
SourceMode: metacache.FlushSourceGrowing,
},
}, nil)
preparer := NewReleaseManualFlushPreparer(manager, wbManager)
prepared, err := preparer.PrepareReleaseManualFlush(ctx, types.PChannelInfo{Name: "pchannel", Term: 1}, 10, "vchannel", releaseSegmentIDs)
assert.NoError(t, err)
assert.True(t, prepared)
}
func TestReleaseManualFlushPreparerNoGrowingProgress(t *testing.T) {
ctx := context.Background()
releaseSegmentIDs := []int64{1001}
extra, err := anypb.New(&message.ManualFlushExtraResponse{})
assert.NoError(t, err)
wal := mock_wal.NewMockWAL(t)
wal.EXPECT().Append(mock.Anything, mock.Anything).Return(&types.AppendResult{
TimeTick: 200,
Extra: extra,
}, nil)
manager := mock_walmanager.NewMockManager(t)
manager.EXPECT().GetAvailableWAL(mock.Anything).Return(wal, nil)
wbManager := writebuffer.NewMockBufferManager(t)
wbManager.EXPECT().UseGrowingSourceFlush("vchannel").Return(true)
wbManager.EXPECT().
GetGrowingFlushProgress(mock.Anything, "vchannel", releaseSegmentIDs, uint64(200)).
Return([]writebuffer.GrowingFlushSegmentProgress{
{
SegmentID: 1001,
NeedReleaseHandoff: false,
SourceMode: metacache.FlushSourceUnknown,
},
}, nil)
preparer := NewReleaseManualFlushPreparer(manager, wbManager)
prepared, err := preparer.PrepareReleaseManualFlush(ctx, types.PChannelInfo{Name: "pchannel", Term: 1}, 10, "vchannel", releaseSegmentIDs)
assert.NoError(t, err)
assert.False(t, prepared)
}
func TestReleaseManualFlushPreparerSkipNotNeeded(t *testing.T) {
ctx := context.Background()
wbManager := &releaseManualFlushCheckBufferManager{
MockBufferManager: writebuffer.NewMockBufferManager(t),
needManualFlush: false,
}
wbManager.EXPECT().UseGrowingSourceFlush("vchannel").Return(true)
preparer := NewReleaseManualFlushPreparer(mock_walmanager.NewMockManager(t), wbManager)
prepared, err := preparer.PrepareReleaseManualFlush(ctx, types.PChannelInfo{Name: "pchannel", Term: 1}, 10, "vchannel", []int64{1001})
assert.NoError(t, err)
assert.False(t, prepared)
}
func TestReleaseManualFlushPreparerSkipNonGrowingSource(t *testing.T) {
ctx := context.Background()
wbManager := writebuffer.NewMockBufferManager(t)
wbManager.EXPECT().UseGrowingSourceFlush("vchannel").Return(false)
preparer := NewReleaseManualFlushPreparer(mock_walmanager.NewMockManager(t), wbManager)
prepared, err := preparer.PrepareReleaseManualFlush(ctx, types.PChannelInfo{Name: "pchannel", Term: 1}, 10, "vchannel", []int64{1001})
assert.NoError(t, err)
assert.False(t, prepared)
}
@@ -0,0 +1,107 @@
package service
import (
"context"
"go.uber.org/zap"
"github.com/milvus-io/milvus/internal/flushcommon/writebuffer"
"github.com/milvus-io/milvus/internal/streamingnode/server/walmanager"
"github.com/milvus-io/milvus/internal/util/streamingutil/status"
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/message"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
)
// NewReleaseManualFlushPreparer creates a process-local release manual flush preparer.
func NewReleaseManualFlushPreparer(walManager walmanager.Manager, writeBufferManager writebuffer.BufferManager) *releaseManualFlushPreparer {
return &releaseManualFlushPreparer{
walManager: walManager,
writeBufferManager: writeBufferManager,
}
}
type releaseManualFlushPreparer struct {
walManager walmanager.Manager
writeBufferManager writebuffer.BufferManager
}
// PrepareReleaseManualFlush appends a normal ManualFlush and retains the requested local growing segments.
func (p *releaseManualFlushPreparer) PrepareReleaseManualFlush(ctx context.Context, pchannel types.PChannelInfo, collectionID int64, vchannel string, releaseSegmentIDs []int64) (bool, error) {
if p.writeBufferManager == nil {
return false, status.NewInner("write buffer manager is not initialized")
}
if vchannel == "" {
return false, status.NewInvaildArgument("vchannel is empty")
}
if collectionID == 0 {
return false, status.NewInvaildArgument("collection id is empty")
}
if len(releaseSegmentIDs) == 0 {
return false, nil
}
if !p.writeBufferManager.UseGrowingSourceFlush(vchannel) {
log.Ctx(ctx).Info("skip release manual flush prepare because channel does not use growing-source flush",
zap.String("vchannel", vchannel),
zap.Int64("collectionID", collectionID),
zap.Int64s("releaseSegmentIDs", releaseSegmentIDs))
return false, nil
}
if checker, ok := p.writeBufferManager.(writebuffer.ReleaseManualFlushNeedChecker); ok {
needManualFlush, err := checker.CheckReleaseManualFlushNeed(ctx, vchannel, releaseSegmentIDs)
if err != nil {
return false, err
}
if !needManualFlush {
log.Ctx(ctx).Info("skip release manual flush prepare because target segments do not need release handoff",
zap.String("vchannel", vchannel),
zap.Int64("collectionID", collectionID),
zap.Int64s("releaseSegmentIDs", releaseSegmentIDs))
return false, nil
}
}
wal, err := p.walManager.GetAvailableWAL(pchannel)
if err != nil {
return false, err
}
flushMsg, err := message.NewManualFlushMessageBuilderV2().
WithVChannel(vchannel).
WithHeader(&message.ManualFlushMessageHeader{
CollectionId: collectionID,
}).
WithBody(&message.ManualFlushMessageBody{}).
BuildMutable()
if err != nil {
return false, err
}
appendResult, err := wal.Append(ctx, flushMsg)
if err != nil {
return false, err
}
var flushMsgResponse message.ManualFlushExtraResponse
if err := appendResult.GetExtra(&flushMsgResponse); err != nil {
return false, err
}
progress, err := p.writeBufferManager.GetGrowingFlushProgress(ctx, vchannel, releaseSegmentIDs, appendResult.TimeTick)
if err != nil {
return false, err
}
prepared := false
for _, segmentProgress := range progress {
if segmentProgress.NeedReleaseHandoff {
prepared = true
break
}
}
log.Ctx(ctx).Info("prepared release manual flush",
zap.String("vchannel", vchannel),
zap.Int64("collectionID", collectionID),
zap.Uint64("flushTs", appendResult.TimeTick),
zap.Int64s("releaseSegmentIDs", releaseSegmentIDs),
zap.Int64s("affectedSegmentIDs", flushMsgResponse.GetSegmentIds()),
zap.Bool("retained", prepared),
zap.Any("progress", progress))
return prepared, nil
}
@@ -212,6 +212,6 @@ func (m *partitionManager) assignSegment(req *AssignSegmentRequest) (*AssignSegm
// There is no segment can be allocated for the insert request.
// Ask a new pending segment to insert.
m.asyncAllocSegment(req.SchemaVersion)
m.asyncAllocSegment(req.SchemaVersion, req.UseGrowingSourceFlush)
return nil, ErrWaitForNewSegment
}
@@ -18,7 +18,7 @@ import (
)
// asyncAllocSegment allocates a new growing segment asynchronously.
func (m *partitionManager) asyncAllocSegment(schemaVersion int32) {
func (m *partitionManager) asyncAllocSegment(schemaVersion int32, useGrowingSourceFlush bool) {
if m.onAllocating != nil {
m.Logger().Debug("segment alloc worker is already on allocating")
// manager is already on allocating.
@@ -27,12 +27,13 @@ func (m *partitionManager) asyncAllocSegment(schemaVersion int32) {
// Create a notifier to notify the waiter when the allocation is done.
m.onAllocating = make(chan struct{})
w := &segmentAllocWorker{
ctx: m.ctx,
collectionID: m.collectionID,
partitionID: m.partitionID,
vchannel: m.vchannel,
wal: m.wal.Get(),
schemaVersion: schemaVersion,
ctx: m.ctx,
collectionID: m.collectionID,
partitionID: m.partitionID,
vchannel: m.vchannel,
wal: m.wal.Get(),
schemaVersion: schemaVersion,
useGrowingSourceFlush: useGrowingSourceFlush,
}
w.SetLogger(m.Logger())
// It should always done asynchronously.
@@ -50,10 +51,11 @@ type segmentAllocWorker struct {
wal wal.WAL
// The following fields are preserved across retries to ensure the same segment
// configuration is used when rebuilding the message after a failed append.
segmentID uint64 // allocated segment ID
storageVersion int64 // storage version determined at first attempt
limitation segmentLimitation // segment limitation determined at first attempt
schemaVersion int32
segmentID uint64 // allocated segment ID
storageVersion int64 // storage version determined at first attempt
limitation segmentLimitation // segment limitation determined at first attempt
schemaVersion int32
useGrowingSourceFlush bool
}
// do is the main loop of the segment allocation worker.
@@ -15,6 +15,7 @@ import (
"github.com/milvus-io/milvus/internal/mocks/streamingnode/server/mock_wal"
"github.com/milvus-io/milvus/internal/mocks/streamingnode/server/wal/interceptors/shard/mock_utils"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/streamingnode/server/resource"
"github.com/milvus-io/milvus/internal/streamingnode/server/wal"
"github.com/milvus-io/milvus/internal/streamingnode/server/wal/interceptors/shard/policy"
@@ -295,6 +296,41 @@ func TestSegmentAllocWorker_InitSegmentConfig(t *testing.T) {
assert.Equal(t, firstLimitation, w.limitation)
}
func TestSegmentAllocWorkerStorageVersionFollowsUseLoonFFI(t *testing.T) {
paramtable.Init()
resource.InitForTest(t)
param := paramtable.Get()
defer param.Reset(param.CommonCfg.UseLoonFFI.Key)
for name, tc := range map[string]struct {
useLoonFFI string
useGrowingSourceFlush bool
expected int64
}{
"v2_without_growing_source": {useLoonFFI: "false", useGrowingSourceFlush: false, expected: storage.StorageV2},
"v2_with_growing_source": {useLoonFFI: "false", useGrowingSourceFlush: true, expected: storage.StorageV2},
"v3_without_growing_source": {useLoonFFI: "true", useGrowingSourceFlush: false, expected: storage.StorageV3},
"v3_with_growing_source": {useLoonFFI: "true", useGrowingSourceFlush: true, expected: storage.StorageV3},
} {
t.Run(name, func(t *testing.T) {
param.Save(param.CommonCfg.UseLoonFFI.Key, tc.useLoonFFI)
w := &segmentAllocWorker{
ctx: context.Background(),
collectionID: 1,
partitionID: 2,
vchannel: "v1",
wal: mock_wal.NewMockWAL(t),
useGrowingSourceFlush: tc.useGrowingSourceFlush,
}
w.SetLogger(log.With())
err := w.initSegmentConfig()
assert.NoError(t, err)
assert.Equal(t, tc.expected, w.storageVersion)
})
}
}
// TestSegmentFlushWorker_WaitForTxnManagerRecoverDone tests the txn manager wait behavior
func TestSegmentFlushWorker_WaitForTxnManagerRecoverDone(t *testing.T) {
paramtable.Init()
@@ -18,7 +18,9 @@ import (
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/proto/streamingpb"
"github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/syncutil"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
var (
@@ -120,8 +122,8 @@ func newSegmentAllocManagersFromRecovery(pchannel types.PChannelInfo, recoverInf
// recover the segment infos from the streaming node segment assignment meta storage
partitionToSegmentManagers := make(map[PartitionUniqueKey]map[int64]*segmentAllocManager)
growingBelongs := make(map[int64]stats.SegmentBelongs)
seenSegments := make(map[int64]struct{}, len(recoverInfos.SegmentAssignments))
for _, rawMeta := range recoverInfos.SegmentAssignments {
m := newSegmentAllocManagerFromProto(pchannel, rawMeta)
coll, ok := collections[rawMeta.GetCollectionId()]
if !ok {
panic(fmt.Sprintf("segment assignment meta is dirty, collection not found, %d", rawMeta.GetCollectionId()))
@@ -129,24 +131,33 @@ func newSegmentAllocManagersFromRecovery(pchannel types.PChannelInfo, recoverInf
if _, ok := coll.PartitionIDs[rawMeta.GetPartitionId()]; !ok {
panic(fmt.Sprintf("segment assignment meta is dirty, partition not found, partition not found, %d", rawMeta.GetPartitionId()))
}
if _, ok := growingBelongs[rawMeta.GetSegmentId()]; ok {
if _, ok := seenSegments[rawMeta.GetSegmentId()]; ok {
panic(fmt.Sprintf("segment assignment meta is dirty, segment repeated, %d", rawMeta.GetSegmentId()))
}
growingBelongs[m.GetSegmentID()] = stats.SegmentBelongs{
PChannel: pchannel.Name,
VChannel: m.GetVChannel(),
CollectionID: rawMeta.GetCollectionId(),
PartitionID: rawMeta.GetPartitionId(),
SegmentID: m.GetSegmentID(),
}
seenSegments[rawMeta.GetSegmentId()] = struct{}{}
uniqueKey := PartitionUniqueKey{
CollectionID: rawMeta.GetCollectionId(),
PartitionID: rawMeta.GetPartitionId(),
}
if _, ok := partitionToSegmentManagers[uniqueKey]; !ok {
partitionToSegmentManagers[uniqueKey] = make(map[int64]*segmentAllocManager, 2)
switch rawMeta.GetState() {
case streamingpb.SegmentAssignmentState_SEGMENT_ASSIGNMENT_STATE_GROWING:
m := newSegmentAllocManagerFromProto(pchannel, rawMeta)
growingBelongs[m.GetSegmentID()] = stats.SegmentBelongs{
PChannel: pchannel.Name,
VChannel: m.GetVChannel(),
CollectionID: rawMeta.GetCollectionId(),
PartitionID: rawMeta.GetPartitionId(),
SegmentID: m.GetSegmentID(),
}
if _, ok := partitionToSegmentManagers[uniqueKey]; !ok {
partitionToSegmentManagers[uniqueKey] = make(map[int64]*segmentAllocManager, 2)
}
partitionToSegmentManagers[uniqueKey][rawMeta.GetSegmentId()] = m
case streamingpb.SegmentAssignmentState_SEGMENT_ASSIGNMENT_STATE_FLUSHED:
continue
default:
panic(fmt.Sprintf("segment assignment meta has unknown state, segment %d state %s", rawMeta.GetSegmentId(), rawMeta.GetState()))
}
partitionToSegmentManagers[uniqueKey][rawMeta.GetSegmentId()] = m
}
return partitionToSegmentManagers, growingBelongs
}
@@ -212,6 +223,22 @@ func (c *CollectionInfo) SchemaVersion() int32 {
return s.GetVersion()
}
func (c *CollectionInfo) UseGrowingSourceFlush() bool {
if c == nil || c.Schema == nil {
return false
}
return typeutil.UseGrowingSourceFlush(c.Schema.GetSchema(),
paramtable.Get().CommonCfg.UseLoonFFI.GetAsBool(),
paramtable.Get().CommonCfg.EnableGrowingSourceFlush.GetAsBool())
}
func (c *CollectionInfo) HasTextField() bool {
if c == nil || c.Schema == nil || c.Schema.GetSchema() == nil {
return false
}
return typeutil.HasTextField(c.Schema.GetSchema())
}
func (m *shardManagerImpl) Channel() types.PChannelInfo {
return m.pchannel
}
@@ -13,12 +13,13 @@ import (
// AssignSegmentRequest is a request to allocate segment.
type AssignSegmentRequest struct {
CollectionID int64
PartitionID int64
ModifiedMetrics stats.ModifiedMetrics
TimeTick uint64
TxnSession TxnSession
SchemaVersion int32
CollectionID int64
PartitionID int64
ModifiedMetrics stats.ModifiedMetrics
TimeTick uint64
TxnSession TxnSession
SchemaVersion int32
UseGrowingSourceFlush bool
}
// AssignSegmentResult is a result of segment allocation.
@@ -142,6 +143,7 @@ func (m *shardManagerImpl) AssignSegment(req *AssignSegmentRequest) (*AssignSegm
// single place that resolves and stamps it before forwarding to partitionManager.
if info := m.collections[req.CollectionID]; info != nil {
req.SchemaVersion = info.SchemaVersion()
req.UseGrowingSourceFlush = info.UseGrowingSourceFlush()
}
result, err := pm.AssignSegment(req)
@@ -1,6 +1,7 @@
package shards
import (
"context"
"testing"
"time"
@@ -12,6 +13,7 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/mocks/streamingnode/server/mock_wal"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/streamingnode/server/resource"
"github.com/milvus-io/milvus/internal/streamingnode/server/wal"
"github.com/milvus-io/milvus/internal/streamingnode/server/wal/interceptors/shard/policy"
@@ -292,6 +294,89 @@ func TestShardManager(t *testing.T) {
m.Close()
}
func TestShardManagerAssignSegmentTextUsesV3CreateSegmentWhenStorageV3Enabled(t *testing.T) {
paramtable.Init()
resource.InitForTest(t)
param := paramtable.Get()
param.Save(param.CommonCfg.UseLoonFFI.Key, "true")
defer param.Reset(param.CommonCfg.UseLoonFFI.Key)
channel := types.PChannelInfo{
Name: "test_text_channel",
Term: 1,
}
appendCh := make(chan *message.CreateSegmentMessageHeader, 1)
w := mock_wal.NewMockWAL(t)
w.EXPECT().Available().RunAndReturn(func() <-chan struct{} {
return make(chan struct{})
}).Maybe()
w.EXPECT().Append(mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, msg message.MutableMessage) (*types.AppendResult, error) {
createMsg := message.MustAsMutableCreateSegmentMessageV2(msg)
appendCh <- createMsg.Header()
return &types.AppendResult{
MessageID: rmq.NewRmqID(1),
TimeTick: 1000,
}, nil
}).Once()
f := syncutil.NewFuture[wal.WAL]()
f.Set(w)
m := RecoverShardManager(&ShardManagerRecoverParam{
ChannelInfo: channel,
WAL: f,
InitialRecoverSnapshot: &recovery.RecoverySnapshot{
VChannels: map[string]*streamingpb.VChannelMeta{
"v_text": {
Vchannel: "v_text",
State: streamingpb.VChannelState_VCHANNEL_STATE_NORMAL,
CollectionInfo: &streamingpb.CollectionInfoOfVChannel{
CollectionId: 10,
Partitions: []*streamingpb.PartitionInfoOfVChannel{
{PartitionId: 20},
},
Schemas: []*streamingpb.CollectionSchemaOfVChannel{
{
Schema: &schemapb.CollectionSchema{
Name: "text_collection",
Version: 7,
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64},
{FieldID: 101, DataType: schemapb.DataType_Text},
},
},
},
},
},
},
},
SegmentAssignments: map[int64]*streamingpb.SegmentAssignmentMeta{},
Checkpoint: &recovery.WALCheckpoint{TimeTick: 100},
},
TxnManager: &mockedTxnManager{},
}).(*shardManagerImpl)
defer m.Close()
_, err := m.AssignSegment(&AssignSegmentRequest{
CollectionID: 10,
PartitionID: 20,
TimeTick: 200,
ModifiedMetrics: stats.ModifiedMetrics{
Rows: 1,
BinarySize: 1,
},
})
assert.ErrorIs(t, err, ErrWaitForNewSegment)
select {
case header := <-appendCh:
assert.Equal(t, storage.StorageV3, header.GetStorageVersion())
assert.Equal(t, int32(7), header.GetSchemaVersion())
case <-time.After(time.Second):
t.Fatal("timed out waiting for create segment message")
}
}
func TestShardManagerSchemaVersionCheck(t *testing.T) {
paramtable.Init()
resource.InitForTest(t)
@@ -369,6 +454,40 @@ func TestShardManagerSchemaVersionCheck(t *testing.T) {
assert.ErrorIs(t, err, ErrCollectionSchemaVersionNotMatch)
assert.Equal(t, int32(1), ver)
textSchema := &schemapb.CollectionSchema{
Name: "test_text_schema_collection",
Version: 4,
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64},
{FieldID: 101, DataType: schemapb.DataType_Text},
},
}
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
createMsgTextSchema := message.NewCreateCollectionMessageBuilderV1().
WithVChannel("v_text_schema").
WithHeader(&message.CreateCollectionMessageHeader{
CollectionId: 104,
PartitionIds: []int64{204},
}).
WithBody(&msgpb.CreateCollectionRequest{
CollectionSchema: textSchema,
}).
MustBuildMutable().
WithTimeTick(250).
WithLastConfirmedUseMessageID().
IntoImmutableMessage(rmq.NewRmqID(13))
m.CreateCollection(message.MustAsImmutableCreateCollectionMessageV1(createMsgTextSchema))
ver, err = m.CheckIfCollectionSchemaVersionMatch(&message.InsertMessageHeader{
CollectionId: 104,
SchemaVersion: proto.Int32(4),
})
assert.NoError(t, err)
assert.Equal(t, int32(4), ver)
assert.True(t, m.collections[104].HasTextField())
assert.True(t, m.collections[104].UseGrowingSourceFlush())
// Test 3: Create collection without schema (legacy), then check version
createMsgNoSchema := message.NewCreateCollectionMessageBuilderV1().
WithVChannel("v_no_schema").
@@ -705,3 +824,50 @@ func TestCollectionInfoSchemaVersion(t *testing.T) {
}
assert.Equal(t, int32(3), ci.SchemaVersion())
}
func TestCollectionInfoUseGrowingSourceFlush(t *testing.T) {
paramtable.Init()
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
assert.False(t, (*CollectionInfo)(nil).UseGrowingSourceFlush())
ci := &CollectionInfo{}
assert.False(t, ci.UseGrowingSourceFlush())
ci.Schema = &streamingpb.CollectionSchemaOfVChannel{}
assert.False(t, ci.UseGrowingSourceFlush())
ci.Schema = &streamingpb.CollectionSchemaOfVChannel{
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64},
},
},
}
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "true")
assert.False(t, ci.UseGrowingSourceFlush())
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
assert.True(t, ci.UseGrowingSourceFlush())
}
func TestCollectionInfoUseGrowingSourceFlush_TextField(t *testing.T) {
paramtable.Init()
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "false")
paramtable.Get().Save(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.UseLoonFFI.Key)
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.EnableGrowingSourceFlush.Key)
ci := &CollectionInfo{
Schema: &streamingpb.CollectionSchemaOfVChannel{
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, DataType: schemapb.DataType_Int64},
{FieldID: 101, DataType: schemapb.DataType_Text},
},
},
},
}
assert.True(t, ci.HasTextField())
assert.False(t, ci.UseGrowingSourceFlush())
paramtable.Get().Save(paramtable.Get().CommonCfg.UseLoonFFI.Key, "true")
assert.True(t, ci.UseGrowingSourceFlush())
}
@@ -19,6 +19,7 @@ import (
"github.com/milvus-io/milvus/internal/streamingnode/server/resource"
"github.com/milvus-io/milvus/internal/streamingnode/server/wal/utility"
internaltypes "github.com/milvus-io/milvus/internal/types"
"github.com/milvus-io/milvus/pkg/v3/log"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/messagespb"
"github.com/milvus-io/milvus/pkg/v3/proto/streamingpb"
@@ -180,6 +181,49 @@ func TestRecoveryStorage(t *testing.T) {
}
}
func TestRecoveryStorageManualFlushMarksSegmentsFlushed(t *testing.T) {
const segmentID = int64(1001)
r := &recoveryStorageImpl{
segments: map[int64]*segmentRecoveryInfo{
segmentID: {
meta: &streamingpb.SegmentAssignmentMeta{
CollectionId: 1,
PartitionId: 2,
SegmentId: segmentID,
Vchannel: "v1",
State: streamingpb.SegmentAssignmentState_SEGMENT_ASSIGNMENT_STATE_GROWING,
Stat: &streamingpb.SegmentAssignmentStat{
ModifiedRows: 10,
ModifiedBinarySize: 100,
CreateSegmentTimeTick: 100,
},
},
},
},
}
r.SetLogger(log.With())
msg := message.NewManualFlushMessageBuilderV2().
WithVChannel("v1").
WithHeader(&message.ManualFlushMessageHeader{
CollectionId: 1,
SegmentIds: []int64{segmentID},
}).
WithBody(&message.ManualFlushMessageBody{}).
MustBuildMutable().
WithTimeTick(200).
WithLastConfirmedUseMessageID().
IntoImmutableMessage(rmq.NewRmqID(2))
r.handleManualFlush(message.MustAsImmutableManualFlushMessageV2(msg))
segment := r.segments[segmentID]
snapshot, shouldBeRemoved := segment.ConsumeDirtyAndGetSnapshot()
require.NotNil(t, snapshot)
assert.True(t, shouldBeRemoved)
assert.Equal(t, streamingpb.SegmentAssignmentState_SEGMENT_ASSIGNMENT_STATE_FLUSHED, snapshot.GetState())
assert.EqualValues(t, 200, snapshot.GetCheckpointTimeTick())
}
type streamBuilder struct {
channel types.PChannelInfo
lastConfirmedMessageID int64
@@ -97,24 +97,26 @@ func (r *recoveryStorageImpl) getSnapshot() *RecoverySnapshot {
if !segment.IsGrowing() {
continue
}
// Defensive filtering: skip GROWING segments whose parent vchannel does not exist
// or is not active, or whose partition has been dropped. This can happen due to
// Defensive filtering: skip recoverable segment assignments whose parent vchannel
// does not exist or is not active, or whose partition has been dropped. This can happen due to
// non-atomic etcd persistence or Kafka offset compaction replaying CreateSegment
// for dropped collections/partitions.
if _, ok := vchannels[segment.meta.Vchannel]; !ok {
r.Logger().Warn("getSnapshot: skipping orphaned growing segment with non-active vchannel",
r.Logger().Warn("getSnapshot: skipping orphaned segment assignment with non-active vchannel",
zap.Int64("segmentID", segmentID),
zap.String("vchannel", segment.meta.Vchannel),
zap.Int64("collectionID", segment.meta.CollectionId),
zap.String("state", segment.meta.State.String()),
)
continue
}
if _, ok := activePartitions[segment.meta.PartitionId]; !ok {
r.Logger().Warn("getSnapshot: skipping orphaned growing segment with dropped partition",
r.Logger().Warn("getSnapshot: skipping orphaned segment assignment with dropped partition",
zap.Int64("segmentID", segmentID),
zap.String("vchannel", segment.meta.Vchannel),
zap.Int64("collectionID", segment.meta.CollectionId),
zap.Int64("partitionID", segment.meta.PartitionId),
zap.String("state", segment.meta.State.String()),
)
continue
}
@@ -85,6 +85,9 @@ func (info *segmentRecoveryInfo) BinarySize() uint64 {
// ObserveInsert is called when an insert message is observed.
func (info *segmentRecoveryInfo) ObserveInsert(timetick uint64, assignment *messagespb.PartitionSegmentAssignment) {
if !info.IsGrowing() {
return
}
if timetick < info.meta.CheckpointTimeTick {
// the txn message will share the same time tick.
// so we only filter the time tick is less than the checkpoint time tick.
@@ -77,10 +77,6 @@ func TestSegmentRecoveryInfo(t *testing.T) {
assert.Nil(t, snapshot)
assert.False(t, shouldBeRemoved)
// insert may came from same txn with same txn.
info.ObserveInsert(ts, assign)
assert.True(t, info.dirty)
ts += 1
info.ObserveFlush(ts)
snapshot, shouldBeRemoved = info.ConsumeDirtyAndGetSnapshot()

Some files were not shown because too many files have changed in this diff Show More