Files
wehub-resource-sync 498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

2678 lines
89 KiB
Go

// 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"
"io"
"math/rand"
"testing"
"time"
"github.com/bytedance/mockey"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"go.uber.org/atomic"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/mocks/util/mock_segcore"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/internal/util/indexparamcheck"
"github.com/milvus-io/milvus/internal/util/initcore"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/mlog"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
"github.com/milvus-io/milvus/pkg/v3/proto/querypb"
"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
"github.com/milvus-io/milvus/pkg/v3/util/hardware"
"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 {
suite.Suite
loader Loader
// Dependencies
manager *Manager
rootPath string
chunkManager storage.ChunkManager
// Data
collectionID int64
partitionID int64
segmentID int64
schema *schemapb.CollectionSchema
segmentNum int
}
type eofRecordReader struct{}
func (eofRecordReader) Next() (storage.Record, error) {
return nil, io.EOF
}
func (eofRecordReader) Close() error {
return nil
}
type deltaLoadTestSegment struct {
Segment
id int64
collectionID int64
deltaData *storage.DeltaData
}
func (s *deltaLoadTestSegment) ID() int64 {
return s.id
}
func (s *deltaLoadTestSegment) Collection() int64 {
return s.collectionID
}
func (s *deltaLoadTestSegment) LastDeltaTimestamp() uint64 {
return 0
}
func (s *deltaLoadTestSegment) LoadDeltaData(ctx context.Context, deltaData *storage.DeltaData) error {
s.deltaData = deltaData
return nil
}
func TestLoadDeltalogsExternalRealPKManifestReadsSourceDeltas(t *testing.T) {
paramtable.Init()
initcore.InitExecExpressionFunctionFactory()
ctx := context.Background()
collectionID := int64(1000)
schema := milvusTableCollectionSchema(false)
schema.ExternalSource = "s3://source-bucket/snapshots/100/metadata/200.json"
manager := NewManager()
err := manager.Collection.PutOrRef(collectionID, schema, nil, &querypb.LoadMetaInfo{
LoadType: querypb.LoadType_LoadCollection,
CollectionID: collectionID,
})
if !assert.NoError(t, err) {
return
}
defer manager.Collection.Unref(collectionID, 1)
loader := &segmentLoader{manager: manager}
segment := &deltaLoadTestSegment{id: 3000, collectionID: collectionID}
sourceDeltaPath := "s3://source-bucket/files/insert_log/1/_delta/100"
manifestPath := packed.MarshalManifestPath("files/insert_log/100/200/300", 1)
patchSourceDeltalogs := mockey.Mock(packed.GetDeltaLogsFromManifestWithExtfs).
Return([]*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogPath: sourceDeltaPath,
EntriesNum: 1,
}},
}}, nil).
Build()
defer patchSourceDeltalogs.UnPatch()
readerCalled := atomic.NewInt32(0)
var gotPathSets [][]string
patchReader := mockey.Mock(storage.NewDeltalogReader).To(
func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
readerCalled.Inc()
gotPathSets = append(gotPathSets, append([]string(nil), paths...))
return eofRecordReader{}, nil
},
).Build()
defer patchReader.UnPatch()
err = loader.loadDeltalogs(ctx, segment, &querypb.SegmentLoadInfo{
SegmentID: segment.id,
CollectionID: collectionID,
ManifestPath: manifestPath,
})
assert.NoError(t, err)
assert.NotNil(t, segment.deltaData)
assert.EqualValues(t, 1, readerCalled.Load())
assert.Equal(t, [][]string{{sourceDeltaPath}}, gotPathSets)
}
func TestLoadDeltalogsExternalRealPKManifestRejectsTargetDeltas(t *testing.T) {
paramtable.Init()
initcore.InitExecExpressionFunctionFactory()
ctx := context.Background()
collectionID := int64(1000)
schema := milvusTableCollectionSchema(false)
schema.ExternalSource = "s3://source-bucket/snapshots/100/metadata/200.json"
manager := NewManager()
err := manager.Collection.PutOrRef(collectionID, schema, nil, &querypb.LoadMetaInfo{
LoadType: querypb.LoadType_LoadCollection,
CollectionID: collectionID,
})
if !assert.NoError(t, err) {
return
}
defer manager.Collection.Unref(collectionID, 1)
loader := &segmentLoader{manager: manager}
segment := &deltaLoadTestSegment{id: 3000, collectionID: collectionID}
targetDeltaPath := "files/insert_log/100/200/300/_delta/88"
manifestPath := packed.MarshalManifestPath("files/insert_log/100/200/300", 1)
patchSourceDeltalogs := mockey.Mock(packed.GetDeltaLogsFromManifestWithExtfs).
Return([]*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogPath: targetDeltaPath,
EntriesNum: 1,
}},
}}, nil).
Build()
defer patchSourceDeltalogs.UnPatch()
readerCalled := atomic.NewInt32(0)
patchReader := mockey.Mock(storage.NewDeltalogReader).To(
func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
readerCalled.Inc()
return eofRecordReader{}, nil
},
).Build()
defer patchReader.UnPatch()
err = loader.loadDeltalogs(ctx, segment, &querypb.SegmentLoadInfo{
SegmentID: segment.id,
CollectionID: collectionID,
ManifestPath: manifestPath,
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "real-PK manifest must not contain target-owned deltalog")
assert.EqualValues(t, 0, readerCalled.Load())
}
func (suite *SegmentLoaderSuite) SetupSuite() {
paramtable.Init()
suite.rootPath = suite.T().Name()
suite.collectionID = rand.Int63()
suite.partitionID = rand.Int63()
suite.segmentID = rand.Int63()
suite.segmentNum = 5
initcore.InitExecExpressionFunctionFactory()
}
func (suite *SegmentLoaderSuite) SetupTest() {
ctx := context.Background()
// TODO:: cpp chunk manager not support local chunk manager
// suite.chunkManager = storage.NewLocalChunkManager(storage.RootPath(
// fmt.Sprintf("/tmp/milvus-ut/%d", rand.Int63())))
chunkManagerFactory := storage.NewTestChunkManagerFactory(paramtable.Get(), suite.rootPath)
suite.chunkManager, _ = chunkManagerFactory.NewPersistentStorageChunkManager(ctx)
// Dependencies
suite.manager = NewManager()
suite.loader = NewLoader(ctx, suite.manager, suite.chunkManager)
initcore.InitRemoteChunkManager(paramtable.Get())
initcore.InitLocalChunkManager(suite.rootPath)
initcore.InitMmapManager(paramtable.Get(), 1)
initcore.InitTieredStorage(paramtable.Get())
initcore.InitLocalArrowFileSystem(suite.rootPath)
initcore.InitRemoteArrowFileSystem(paramtable.Get())
// Data
suite.schema = mock_segcore.GenTestCollectionSchema("test", schemapb.DataType_Int64, false)
indexMeta := mock_segcore.GenTestIndexMeta(suite.collectionID, suite.schema)
loadMeta := &querypb.LoadMetaInfo{
LoadType: querypb.LoadType_LoadCollection,
CollectionID: suite.collectionID,
PartitionIDs: []int64{suite.partitionID},
}
suite.manager.Collection.PutOrRef(suite.collectionID, suite.schema, indexMeta, loadMeta)
}
func (suite *SegmentLoaderSuite) TearDownTest() {
ctx := context.Background()
for i := 0; i < suite.segmentNum; i++ {
suite.manager.Segment.Remove(context.Background(), suite.segmentID+int64(i), querypb.DataScope_All)
}
suite.chunkManager.RemoveWithPrefix(ctx, suite.rootPath)
}
func (suite *SegmentLoaderSuite) TestLoad() {
ctx := context.Background()
msgLength := 4
// Load sealed
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.NoError(err)
// Load growing
binlogs, statsLogs, err = mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID+1,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID + 1,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.NoError(err)
}
func (suite *SegmentLoaderSuite) TestLoadFail() {
ctx := context.Background()
msgLength := 4
// Load sealed
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
// make file & binlog mismatch
for _, binlog := range binlogs {
for _, log := range binlog.GetBinlogs() {
log.LogPath = log.LogPath + "-suffix"
}
}
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Error(err)
}
func (suite *SegmentLoaderSuite) TestLoadMultipleSegments() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
// Load sealed
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, loadInfos...)
suite.NoError(err)
// Will load bloom filter with sealed segments
for _, segment := range segments {
for pk := 0; pk < 100; pk++ {
lc := storage.NewLocationsCache(storage.NewInt64PrimaryKey(int64(pk)))
pkReady := segment.PkCandidateExist()
suite.Require().True(pkReady)
exist := segment.MayPkExist(lc)
suite.Require().True(exist)
}
}
// Load growing
loadInfos = loadInfos[:0]
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(suite.segmentNum) + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, loadInfos...)
suite.NoError(err)
// Should load bloom filter with growing segments
for _, segment := range segments {
for pk := 0; pk < 100; pk++ {
lc := storage.NewLocationsCache(storage.NewInt64PrimaryKey(int64(pk)))
pkReady := segment.PkCandidateExist()
suite.True(pkReady)
exist := segment.MayPkExist(lc)
suite.True(exist)
}
}
}
func (suite *SegmentLoaderSuite) TestLoadWithIndex() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
// Load sealed
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
indexInfo, err := mock_segcore.GenAndSaveIndex(
suite.collectionID,
suite.partitionID,
segmentID,
vecFields[0],
msgLength,
mock_segcore.IndexFaissIVFFlat,
metric.L2,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
IndexInfos: []*querypb.FieldIndexInfo{indexInfo},
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, loadInfos...)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
for _, segment := range segments {
suite.True(segment.ExistIndex(vecFields[0]))
}
}
func (suite *SegmentLoaderSuite) TestLoadWithIndexPreferFieldDataWhenIndexHasRawData() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
oldPreferFieldData := paramtable.Get().QueryNodeCfg.PreferFieldDataWhenIndexHasRawData.SwapTempValue("true")
initcore.SyncPreferFieldDataWhenIndexHasRawData(ctx, paramtable.Get())
defer func() {
paramtable.Get().QueryNodeCfg.PreferFieldDataWhenIndexHasRawData.SwapTempValue(oldPreferFieldData)
initcore.SyncPreferFieldDataWhenIndexHasRawData(ctx, paramtable.Get())
}()
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
indexInfo, err := mock_segcore.GenAndSaveIndex(
suite.collectionID,
suite.partitionID,
segmentID,
vecFields[0],
msgLength,
mock_segcore.IndexFaissIVFFlat,
metric.L2,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
IndexInfos: []*querypb.FieldIndexInfo{indexInfo},
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, loadInfos...)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
for _, segment := range segments {
suite.True(segment.ExistIndex(vecFields[0]))
suite.True(segment.HasRawData(vecFields[0]))
localSegment, ok := segment.(*LocalSegment)
suite.True(ok)
suite.True(localSegment.HasFieldData(vecFields[0]))
}
}
// Negative counterpart: with the flag off (default) and the index already
// holding raw data, the loader must skip loading the field data — otherwise
// the memory-saving behavior relied on by existing deployments is broken.
func (suite *SegmentLoaderSuite) TestLoadWithIndexSkipsFieldDataByDefault() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
oldPreferFieldData := paramtable.Get().QueryNodeCfg.PreferFieldDataWhenIndexHasRawData.SwapTempValue("false")
initcore.SyncPreferFieldDataWhenIndexHasRawData(ctx, paramtable.Get())
defer func() {
paramtable.Get().QueryNodeCfg.PreferFieldDataWhenIndexHasRawData.SwapTempValue(oldPreferFieldData)
initcore.SyncPreferFieldDataWhenIndexHasRawData(ctx, paramtable.Get())
}()
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
indexInfo, err := mock_segcore.GenAndSaveIndex(
suite.collectionID,
suite.partitionID,
segmentID,
vecFields[0],
msgLength,
mock_segcore.IndexFaissIVFFlat,
metric.L2,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
IndexInfos: []*querypb.FieldIndexInfo{indexInfo},
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, loadInfos...)
suite.NoError(err)
vecFields := funcutil.GetVecFieldIDs(suite.schema)
for _, segment := range segments {
suite.True(segment.ExistIndex(vecFields[0]))
suite.True(segment.HasRawData(vecFields[0]))
localSegment, ok := segment.(*LocalSegment)
suite.True(ok)
suite.False(localSegment.HasFieldData(vecFields[0]))
}
}
func (suite *SegmentLoaderSuite) TestLoadBloomFilter() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
// Load sealed
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
bfs, err := suite.loader.LoadBloomFilterSet(ctx, suite.collectionID, loadInfos...)
suite.NoError(err)
for _, bf := range bfs {
for pk := 0; pk < 100; pk++ {
lc := storage.NewLocationsCache(storage.NewInt64PrimaryKey(int64(pk)))
exist := bf.MayPkExist(lc)
suite.Require().True(exist)
}
}
}
func (suite *SegmentLoaderSuite) TestLoadWithoutBloomFilter() {
paramtable.Get().Save(paramtable.Get().CommonCfg.BloomFilterEnabled.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.BloomFilterEnabled.Key)
ctx := context.Background()
msgLength := 100
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, loadInfo)
suite.NoError(err)
suite.Len(segments, 1)
suite.False(segments[0].PkCandidateExist())
bfs, err := suite.loader.LoadBloomFilterSet(ctx, suite.collectionID, loadInfo)
suite.NoError(err)
suite.NotNil(bfs)
suite.Len(bfs, 1)
suite.False(bfs[0].PkCandidateExist()) // metadata-only stub when BF disabled
}
func (suite *SegmentLoaderSuite) TestLoadDeltaLogs() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
// Load sealed
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
// Delete PKs 1, 2
deltaLogs, err := mock_segcore.SaveDeltaLog(suite.collectionID,
suite.partitionID,
segmentID,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
Deltalogs: deltaLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, loadInfos...)
suite.NoError(err)
for _, segment := range segments {
suite.Equal(int64(100-2), segment.RowNum())
for pk := 0; pk < 100; pk++ {
if pk == 1 || pk == 2 {
continue
}
lc := storage.NewLocationsCache(storage.NewInt64PrimaryKey(int64(pk)))
exist := segment.MayPkExist(lc)
suite.Require().True(exist)
}
}
}
func (suite *SegmentLoaderSuite) TestLoadVirtualPKExternalCollectionLoadsDeltaLogs() {
ctx := context.Background()
suite.schema.ExternalSource = "s3://bucket/source"
suite.schema.ExternalSpec = `{"format":"milvus-table"}`
pkField := GetPkField(suite.schema)
suite.Require().NotNil(pkField)
pkField.Name = common.VirtualPKFieldName
suite.Require().NoError(suite.manager.Collection.UpdateSchema(suite.collectionID, suite.schema, 1))
msgLength := 100
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
deltaLogs, err := mock_segcore.SaveDeltaLog(suite.collectionID,
suite.partitionID,
suite.segmentID,
suite.chunkManager,
)
suite.Require().NoError(err)
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
Deltalogs: deltaLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Require().NoError(err)
suite.Require().Len(segments, 1)
suite.Equal(int64(msgLength-2), segments[0].RowNum())
}
func (suite *SegmentLoaderSuite) TestLoadDupDeltaLogs() {
ctx := context.Background()
loadInfos := make([]*querypb.SegmentLoadInfo, 0, suite.segmentNum)
msgLength := 100
// Load sealed
for i := 0; i < suite.segmentNum; i++ {
segmentID := suite.segmentID + int64(i)
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
// Delete PKs 1, 2
deltaLogs, err := mock_segcore.SaveDeltaLog(suite.collectionID,
suite.partitionID,
segmentID,
suite.chunkManager,
)
suite.NoError(err)
loadInfos = append(loadInfos, &querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
Deltalogs: deltaLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
}
segments, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, loadInfos...)
suite.NoError(err)
for i, segment := range segments {
suite.Equal(int64(100-2), segment.RowNum())
for pk := 0; pk < 100; pk++ {
if pk == 1 || pk == 2 {
continue
}
lc := storage.NewLocationsCache(storage.NewInt64PrimaryKey(int64(pk)))
exist := segment.MayPkExist(lc)
suite.Require().True(exist)
}
seg := segment.(*LocalSegment)
// nothing would happen as the delta logs have been all applied,
// so the released segment won't cause error
seg.Release(ctx)
loadInfos[i].Deltalogs[0].Binlogs[0].TimestampTo--
err := suite.loader.LoadDeltaLogs(ctx, seg, loadInfos[i])
suite.NoError(err)
}
}
// TestLoadDeltaLogsV3PlaceholderSkipsPathRead verifies that for V3 (manifest)
// segments, the pathless Deltalogs summary placeholder in SegmentLoadInfo is
// treated as metadata-only and does NOT trigger the V1 path-based delta
// download loop. The real delta data for V3 segments is loaded via
// NewDeltalogReaderFromManifest instead.
func (suite *SegmentLoaderSuite) TestLoadDeltaLogsV3PlaceholderSkipsPathRead() {
ctx := context.Background()
// Load a base segment so we have a concrete LocalSegment to run loadDeltalogs against.
msgLength := 4
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
segs, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Require().NoError(err)
suite.Require().Len(segs, 1)
segment := segs[0]
readerCalled := atomic.NewInt32(0)
manifestCalled := atomic.NewInt32(0)
patchManifest := mockey.Mock(packed.GetDeltaLogPathsFromManifest).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig) ([]string, error) {
manifestCalled.Inc()
// Return empty paths — no delta data in manifest.
return nil, nil
},
).Build()
defer patchManifest.UnPatch()
patchReader := mockey.Mock(storage.NewDeltalogReader).To(
func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
readerCalled.Inc()
return nil, errors.New("should not be called when manifest has no delta paths")
},
).Build()
defer patchReader.UnPatch()
v3LoadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
ManifestPath: "/tmp/fake/manifest.json?version=0",
Deltalogs: []*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogID: 1234,
EntriesNum: 10,
MemorySize: 1024,
// LogPath intentionally empty — this is the summary placeholder.
}},
}},
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
loader := suite.loader.(*segmentLoader)
err = loader.loadDeltalogs(ctx, segment, v3LoadInfo)
suite.NoError(err)
suite.EqualValues(1, manifestCalled.Load(),
"GetDeltaLogPathsFromManifest must be called for V3 segments")
suite.EqualValues(0, readerCalled.Load(),
"NewDeltalogReader must not be called when manifest returns no paths")
}
func (suite *SegmentLoaderSuite) TestLoadDeltaLogsExternalRealPKManifestStorageV3DeltasUseFFIReader() {
ctx := context.Background()
msgLength := 4
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
segs, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Require().NoError(err)
suite.Require().Len(segs, 1)
segment := segs[0]
suite.schema.ExternalSource = "s3://source-bucket/snapshots/100/metadata/200.json"
suite.schema.ExternalSpec = `{"format":"milvus-table"}`
pkField := GetPkField(suite.schema)
suite.Require().NotNil(pkField)
pkField.ExternalField = pkField.GetName()
suite.Require().NoError(suite.manager.Collection.UpdateSchema(suite.collectionID, suite.schema, 1))
sourceDeltaPath := "s3://source-bucket/files/insert_log/1/_delta/100"
manifestPath := packed.MarshalManifestPath("files/insert_log/100/200/300", 1)
sourceDeltalogsCalled := atomic.NewInt32(0)
patchSourceDeltalogs := mockey.Mock(packed.GetDeltaLogsFromManifestWithExtfs).To(
func(gotManifestPath string, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) ([]*datapb.FieldBinlog, error) {
sourceDeltalogsCalled.Inc()
suite.Equal(manifestPath, gotManifestPath)
suite.Equal(suite.collectionID, extfs.CollectionID)
suite.Equal(suite.schema.GetExternalSource(), extfs.Source)
suite.Equal(suite.schema.GetExternalSpec(), extfs.Spec)
return []*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogPath: sourceDeltaPath,
EntriesNum: 1,
}},
}}, nil
},
).Build()
defer patchSourceDeltalogs.UnPatch()
readerCalled := atomic.NewInt32(0)
patchReader := mockey.Mock(storage.NewDeltalogReader).To(
func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
readerCalled.Inc()
suite.Equal(schemapb.DataType_Int64, pkType)
suite.Equal([]string{sourceDeltaPath}, paths)
return eofRecordReader{}, nil
},
).Build()
defer patchReader.UnPatch()
err = suite.loader.(*segmentLoader).loadDeltalogs(ctx, segment, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
ManifestPath: manifestPath,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.NoError(err)
suite.EqualValues(1, sourceDeltalogsCalled.Load())
suite.EqualValues(1, readerCalled.Load())
}
func (suite *SegmentLoaderSuite) TestLoadDeltaLogsExternalRealPKManifestLegacyL0UsesLegacyReader() {
ctx := context.Background()
msgLength := 4
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
segs, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Require().NoError(err)
suite.Require().Len(segs, 1)
segment := segs[0]
suite.schema.ExternalSource = "s3://source-bucket/snapshots/100/metadata/200.json"
suite.schema.ExternalSpec = `{"format":"milvus-table"}`
pkField := GetPkField(suite.schema)
suite.Require().NotNil(pkField)
pkField.ExternalField = pkField.GetName()
suite.Require().NoError(suite.manager.Collection.UpdateSchema(suite.collectionID, suite.schema, 1))
sourceDeltaPath := "s3://source-bucket/files/delta_log/1/2/3/100"
manifestPath := packed.MarshalManifestPath("files/insert_log/100/200/300", 1)
patchSourceDeltalogs := mockey.Mock(packed.GetDeltaLogsFromManifestWithExtfs).
Return([]*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogPath: sourceDeltaPath,
EntriesNum: 1,
}},
}}, nil).
Build()
defer patchSourceDeltalogs.UnPatch()
legacyReaderCalled := atomic.NewInt32(0)
patchReader := mockey.Mock(storage.NewDeltalogReader).
To(func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
legacyReaderCalled.Inc()
suite.Equal(schemapb.DataType_Int64, pkType)
suite.Equal([]string{sourceDeltaPath}, paths)
return eofRecordReader{}, nil
}).Build()
defer patchReader.UnPatch()
err = suite.loader.(*segmentLoader).loadDeltalogs(ctx, segment, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
ManifestPath: manifestPath,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.NoError(err)
suite.EqualValues(1, legacyReaderCalled.Load())
}
func TestReadExternalFiles(t *testing.T) {
storageConfig := &indexpb.StorageConfig{StorageType: "local"}
extfs := packed.ExternalSpecContext{
CollectionID: 100,
Source: "s3://bucket/source",
Spec: `{"format":"milvus-table"}`,
}
paths := []string{
"s3://bucket/source/_delta/1",
"files/insert_log/100/10/20/_delta/2",
}
var gotPaths []string
var gotExtfs []packed.ExternalSpecContext
patchRead := mockey.Mock(packed.ReadFileWithExternalSpec).
To(func(sc *indexpb.StorageConfig, filePath string, ctx packed.ExternalSpecContext) ([]byte, error) {
assert.Same(t, storageConfig, sc)
gotPaths = append(gotPaths, filePath)
gotExtfs = append(gotExtfs, ctx)
return []byte("delta:" + filePath), nil
}).Build()
defer patchRead.UnPatch()
data, err := readExternalFiles(context.Background(), storageConfig, extfs, paths)
assert.NoError(t, err)
assert.Equal(t, paths, gotPaths)
assert.Equal(t, []packed.ExternalSpecContext{extfs, extfs}, gotExtfs)
assert.Equal(t, [][]byte{
[]byte("delta:s3://bucket/source/_delta/1"),
[]byte("delta:files/insert_log/100/10/20/_delta/2"),
}, data)
}
func (suite *SegmentLoaderSuite) makeExternalRealPKBFLoadInfo(ctx context.Context) (*querypb.SegmentLoadInfo, []string, []byte) {
suite.schema.ExternalSource = "s3://bucket/source"
suite.schema.ExternalSpec = `{"format":"milvus-table"}`
pkField := GetPkField(suite.schema)
_, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
10,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
suite.Require().Len(statsLogs, 1)
suite.Require().Len(statsLogs[0].GetBinlogs(), 1)
localStatsPath := statsLogs[0].GetBinlogs()[0].GetLogPath()
payload, err := suite.chunkManager.Read(ctx, localStatsPath)
suite.Require().NoError(err)
externalStatsPaths := []string{
fmt.Sprintf("s3://bucket/source/_stats/bloom_filter.%d/1001", pkField.GetFieldID()),
fmt.Sprintf("s3://bucket/source/_stats/bloom_filter.%d/1002", pkField.GetFieldID()),
}
statsLogs[0].FieldID = pkField.GetFieldID()
statsLogs[0].GetBinlogs()[0].LogPath = externalStatsPaths[0]
statsLogs[0].GetBinlogs()[0].MemorySize = int64(len(payload))
statsLogs[0].Binlogs = append(statsLogs[0].GetBinlogs(), &datapb.Binlog{
LogID: 1002,
LogPath: externalStatsPaths[1],
MemorySize: int64(len(payload)),
})
return &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
Statslogs: statsLogs,
StorageVersion: storage.StorageV3,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}, externalStatsPaths, payload
}
func (suite *SegmentLoaderSuite) TestLoadSingleBloomFilterSetExternalRealPKUsesExternalStats() {
ctx := context.Background()
loadInfo, externalStatsPaths, payload := suite.makeExternalRealPKBFLoadInfo(ctx)
readCalls := atomic.NewInt32(0)
var gotPaths []string
patchRead := mockey.Mock(packed.ReadFileWithExternalSpec).
To(func(sc *indexpb.StorageConfig, filePath string, extfs packed.ExternalSpecContext) ([]byte, error) {
readCalls.Inc()
gotPaths = append(gotPaths, filePath)
suite.Equal(suite.collectionID, extfs.CollectionID)
suite.Equal(suite.schema.GetExternalSource(), extfs.Source)
suite.Equal(suite.schema.GetExternalSpec(), extfs.Spec)
return payload, nil
}).Build()
defer patchRead.UnPatch()
bfs, err := suite.loader.(*segmentLoader).loadSingleBloomFilterSet(ctx, suite.collectionID, loadInfo, SegmentTypeSealed)
suite.NoError(err)
suite.EqualValues(2, readCalls.Load())
suite.Equal(externalStatsPaths, gotPaths)
suite.True(bfs.PkCandidateExist())
suite.True(bfs.MayPkExist(storage.NewLocationsCache(storage.NewInt64PrimaryKey(1))))
}
func (suite *SegmentLoaderSuite) TestLoadSingleBloomFilterSetExternalRealPKIgnoresBloomFilterDisable() {
paramtable.Get().Save(paramtable.Get().CommonCfg.BloomFilterEnabled.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.BloomFilterEnabled.Key)
ctx := context.Background()
loadInfo, externalStatsPaths, payload := suite.makeExternalRealPKBFLoadInfo(ctx)
readCalls := atomic.NewInt32(0)
var gotPaths []string
patchRead := mockey.Mock(packed.ReadFileWithExternalSpec).
To(func(sc *indexpb.StorageConfig, filePath string, extfs packed.ExternalSpecContext) ([]byte, error) {
readCalls.Inc()
gotPaths = append(gotPaths, filePath)
return payload, nil
}).Build()
defer patchRead.UnPatch()
bfs, err := suite.loader.(*segmentLoader).loadSingleBloomFilterSet(ctx, suite.collectionID, loadInfo, SegmentTypeSealed)
suite.NoError(err)
suite.EqualValues(2, readCalls.Load())
suite.Equal(externalStatsPaths, gotPaths)
suite.True(bfs.PkCandidateExist())
suite.True(bfs.MayPkExist(storage.NewLocationsCache(storage.NewInt64PrimaryKey(1))))
}
func (suite *SegmentLoaderSuite) TestLoadBloomFilterSetExternalRealPKUsesExternalStats() {
ctx := context.Background()
loadInfo, externalStatsPaths, payload := suite.makeExternalRealPKBFLoadInfo(ctx)
readCalls := atomic.NewInt32(0)
var gotPaths []string
patchRead := mockey.Mock(packed.ReadFileWithExternalSpec).
To(func(sc *indexpb.StorageConfig, filePath string, extfs packed.ExternalSpecContext) ([]byte, error) {
readCalls.Inc()
gotPaths = append(gotPaths, filePath)
suite.Equal(suite.collectionID, extfs.CollectionID)
suite.Equal(suite.schema.GetExternalSource(), extfs.Source)
suite.Equal(suite.schema.GetExternalSpec(), extfs.Spec)
return payload, nil
}).Build()
defer patchRead.UnPatch()
bfs, err := suite.loader.LoadBloomFilterSet(ctx, suite.collectionID, loadInfo)
suite.NoError(err)
suite.Len(bfs, 1)
suite.EqualValues(2, readCalls.Load())
suite.Equal(externalStatsPaths, gotPaths)
suite.True(bfs[0].PkCandidateExist())
suite.True(bfs[0].MayPkExist(storage.NewLocationsCache(storage.NewInt64PrimaryKey(1))))
}
func (suite *SegmentLoaderSuite) TestLoadBloomFilterSetExternalRealPKIgnoresBloomFilterDisable() {
paramtable.Get().Save(paramtable.Get().CommonCfg.BloomFilterEnabled.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().CommonCfg.BloomFilterEnabled.Key)
ctx := context.Background()
loadInfo, externalStatsPaths, payload := suite.makeExternalRealPKBFLoadInfo(ctx)
readCalls := atomic.NewInt32(0)
var gotPaths []string
patchRead := mockey.Mock(packed.ReadFileWithExternalSpec).
To(func(sc *indexpb.StorageConfig, filePath string, extfs packed.ExternalSpecContext) ([]byte, error) {
readCalls.Inc()
gotPaths = append(gotPaths, filePath)
return payload, nil
}).Build()
defer patchRead.UnPatch()
bfs, err := suite.loader.LoadBloomFilterSet(ctx, suite.collectionID, loadInfo)
suite.NoError(err)
suite.Len(bfs, 1)
suite.EqualValues(2, readCalls.Load())
suite.Equal(externalStatsPaths, gotPaths)
suite.True(bfs[0].PkCandidateExist())
suite.True(bfs[0].MayPkExist(storage.NewLocationsCache(storage.NewInt64PrimaryKey(1))))
}
// TestLoadDeltaLogsV1StillUsesPathRead ensures the skip logic does not break
// the legacy V1 path: when ManifestPath is empty, the path-based delta reader
// is still exercised.
func (suite *SegmentLoaderSuite) TestLoadDeltaLogsV1StillUsesPathRead() {
ctx := context.Background()
msgLength := 4
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.Require().NoError(err)
deltaLogs, err := mock_segcore.SaveDeltaLog(suite.collectionID,
suite.partitionID,
suite.segmentID,
suite.chunkManager,
)
suite.Require().NoError(err)
segs, err := suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.Require().NoError(err)
suite.Require().Len(segs, 1)
segment := segs[0]
readerCalled := atomic.NewInt32(0)
manifestCalled := atomic.NewInt32(0)
patchManifest := mockey.Mock(packed.GetDeltaLogPathsFromManifest).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig) ([]string, error) {
manifestCalled.Inc()
return nil, nil
},
).Build()
defer patchManifest.UnPatch()
patchReader := mockey.Mock(storage.NewDeltalogReader).To(
func(pkType schemapb.DataType, paths []string, option ...storage.RwOption) (storage.RecordReader, error) {
readerCalled.Inc()
return nil, io.EOF
},
).Build()
defer patchReader.UnPatch()
v1LoadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
Deltalogs: deltaLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
// ManifestPath left empty → V1 path.
}
loader := suite.loader.(*segmentLoader)
_ = loader.loadDeltalogs(ctx, segment, v1LoadInfo)
suite.Greater(readerCalled.Load(), int32(0),
"NewDeltalogReader must be invoked for V1 segments")
suite.EqualValues(0, manifestCalled.Load(),
"GetDeltaLogPathsFromManifest must not be called when ManifestPath is empty")
}
func (suite *SegmentLoaderSuite) TestLoadIndex() {
ctx := context.Background()
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 1,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
IndexInfos: []*querypb.FieldIndexInfo{
{
IndexFilePaths: []string{},
},
},
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
segment := &LocalSegment{
baseSegment: baseSegment{loadInfo: atomic.NewPointer[querypb.SegmentLoadInfo](loadInfo)},
bm25StatsHolder: newBM25StatsHolder(),
}
err := suite.loader.LoadIndex(ctx, segment, loadInfo, 0)
suite.ErrorIs(err, merr.ErrIndexNotFound)
}
func (suite *SegmentLoaderSuite) TestLoadIndexWithLimitedResource() {
ctx := context.Background()
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 1,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
IndexInfos: []*querypb.FieldIndexInfo{
{
FieldID: 1,
IndexFilePaths: []string{},
IndexParams: []*commonpb.KeyValuePair{
{
Key: common.IndexTypeKey,
Value: indexparamcheck.IndexINVERTED,
},
},
},
},
BinlogPaths: []*datapb.FieldBinlog{
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{
LogSize: 1000000000,
MemorySize: 1000000000,
},
},
},
},
}
segment := &LocalSegment{
baseSegment: baseSegment{loadInfo: atomic.NewPointer[querypb.SegmentLoadInfo](loadInfo)},
bm25StatsHolder: newBM25StatsHolder(),
}
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.DiskCapacityLimit.Key, "100000")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.DiskCapacityLimit.Key)
err := suite.loader.LoadIndex(ctx, segment, loadInfo, 0)
suite.Error(err)
}
func (suite *SegmentLoaderSuite) TestLoadWithMmap() {
key := paramtable.Get().QueryNodeCfg.MmapDirPath.Key
paramtable.Get().Save(key, "/tmp/mmap-test")
defer paramtable.Get().Reset(key)
ctx := context.Background()
collection := suite.manager.Collection.Get(suite.collectionID)
for _, field := range collection.Schema().GetFields() {
field.TypeParams = append(field.TypeParams, &commonpb.KeyValuePair{
Key: common.MmapEnabledKey,
Value: "true",
})
}
msgLength := 100
// Load sealed
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
suite.NoError(err)
}
func (suite *SegmentLoaderSuite) TestRunOutMemory() {
ctx := context.Background()
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.OverloadedMemoryThresholdPercentage.Key, "0")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.OverloadedMemoryThresholdPercentage.Key)
msgLength := 4
// Load sealed
binlogs, statsLogs, err := mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
// TODO: this case should be fixed!
// currently the binlog size is all zero, this expected error is triggered by interim index usage calculation instead of binlog size
if !paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool() {
suite.Error(err)
}
// Load growing
binlogs, statsLogs, err = mock_segcore.SaveBinLog(ctx,
suite.collectionID,
suite.partitionID,
suite.segmentID+1,
msgLength,
suite.schema,
suite.chunkManager,
)
suite.NoError(err)
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID + 1,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
if !paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool() {
suite.Error(err)
}
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapDirPath.Key, "./mmap")
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeSealed, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
if !paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool() {
suite.Error(err)
}
_, err = suite.loader.Load(ctx, suite.collectionID, SegmentTypeGrowing, 0, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID + 1,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
BinlogPaths: binlogs,
Statslogs: statsLogs,
NumOfRows: int64(msgLength),
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
if !paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool() {
suite.Error(err)
}
}
type SegmentLoaderDetailSuite struct {
suite.Suite
loader *segmentLoader
manager *Manager
rootPath string
chunkManager storage.ChunkManager
// Data
collectionID int64
partitionID int64
segmentID int64
schema *schemapb.CollectionSchema
segmentNum int
}
func (suite *SegmentLoaderDetailSuite) SetupSuite() {
paramtable.Init()
suite.rootPath = suite.T().Name()
suite.collectionID = rand.Int63()
suite.partitionID = rand.Int63()
suite.segmentID = rand.Int63()
suite.segmentNum = 5
suite.schema = mock_segcore.GenTestCollectionSchema("test", schemapb.DataType_Int64, false)
}
func (suite *SegmentLoaderDetailSuite) SetupTest() {
// Dependencies
suite.manager = NewManager()
ctx := context.Background()
chunkManagerFactory := storage.NewTestChunkManagerFactory(paramtable.Get(), suite.rootPath)
suite.chunkManager, _ = chunkManagerFactory.NewPersistentStorageChunkManager(ctx)
suite.loader = NewLoader(ctx, suite.manager, suite.chunkManager)
initcore.InitRemoteChunkManager(paramtable.Get())
// Data
schema := mock_segcore.GenTestCollectionSchema("test", schemapb.DataType_Int64, false)
indexMeta := mock_segcore.GenTestIndexMeta(suite.collectionID, schema)
loadMeta := &querypb.LoadMetaInfo{
LoadType: querypb.LoadType_LoadCollection,
CollectionID: suite.collectionID,
PartitionIDs: []int64{suite.partitionID},
}
suite.Require().NoError(suite.manager.Collection.PutOrRef(suite.collectionID, schema, indexMeta, loadMeta))
}
func (suite *SegmentLoaderDetailSuite) TestWaitSegmentLoadDone() {
suite.Run("wait_success", func() {
infos := suite.loader.prepare(context.Background(), SegmentTypeSealed, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 100,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
go func() {
<-time.After(time.Second)
suite.loader.notifyLoadFinish(infos...)
}()
err := suite.loader.waitSegmentLoadDone(context.Background(), SegmentTypeSealed, []int64{suite.segmentID}, 0)
suite.NoError(err)
})
suite.Run("wait_failure", func() {
suite.SetupTest()
infos := suite.loader.prepare(context.Background(), SegmentTypeSealed, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 100,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
go func() {
<-time.After(time.Second)
suite.loader.unregister(infos...)
}()
err := suite.loader.waitSegmentLoadDone(context.Background(), SegmentTypeSealed, []int64{suite.segmentID}, 0)
suite.Error(err)
})
suite.Run("wait_timeout", func() {
suite.SetupTest()
suite.loader.prepare(context.Background(), SegmentTypeSealed, &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 100,
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
})
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := suite.loader.waitSegmentLoadDone(ctx, SegmentTypeSealed, []int64{suite.segmentID}, 0)
suite.Error(err)
suite.True(merr.IsCanceledOrTimeout(err))
})
}
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",
}
segMgr := &segmentManager{}
getWithTypeCalled := atomic.NewInt32(0)
patchGetWithType := mockey.Mock(mockey.GetMethod(segMgr, "GetWithType")).
To(func(segmentID typeutil.UniqueID, typ SegmentType) Segment {
getWithTypeCalled.Inc()
assert.EqualValues(t, loadInfo.GetSegmentID(), segmentID)
assert.Equal(t, SegmentTypeGrowing, typ)
return nil
}).
Build()
defer patchGetWithType.UnPatch()
loader := &segmentLoader{
manager: &Manager{
Segment: segMgr,
},
loadingSegments: typeutil.NewConcurrentMap[int64, *loadResult](),
}
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))
assert.EqualValues(t, 1, getWithTypeCalled.Load())
}
func TestConfigureUseTakeForOutput(t *testing.T) {
paramtable.Init()
internalKey := paramtable.Get().QueryNodeCfg.InternalCollectionUseTakeForOutput.Key
externalKey := paramtable.Get().QueryNodeCfg.ExternalCollectionUseTakeForOutput.Key
paramtable.Get().Reset(internalKey)
paramtable.Get().Reset(externalKey)
defer paramtable.Get().Reset(internalKey)
defer paramtable.Get().Reset(externalKey)
t.Run("nil load info", func(t *testing.T) {
assert.NotPanics(t, func() {
configureUseTakeForOutput(nil, &schemapb.CollectionSchema{})
})
})
t.Run("internal collection disabled by default", func(t *testing.T) {
loadInfo := &querypb.SegmentLoadInfo{}
configureUseTakeForOutput(loadInfo, &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64}},
})
assert.False(t, loadInfo.GetUseTakeForOutput())
})
t.Run("internal collection uses internal switch", func(t *testing.T) {
paramtable.Get().Save(internalKey, "true")
defer paramtable.Get().Reset(internalKey)
loadInfo := &querypb.SegmentLoadInfo{}
configureUseTakeForOutput(loadInfo, &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64}},
})
assert.True(t, loadInfo.GetUseTakeForOutput())
})
t.Run("external collection uses external switch", func(t *testing.T) {
paramtable.Get().Save(externalKey, "false")
defer paramtable.Get().Reset(externalKey)
loadInfo := &querypb.SegmentLoadInfo{}
configureUseTakeForOutput(loadInfo, &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64, ExternalField: "id"}},
})
assert.False(t, loadInfo.GetUseTakeForOutput())
})
}
func (suite *SegmentLoaderDetailSuite) TestRequestResource() {
suite.Run("out_of_memory_zero_info", func() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.OverloadedMemoryThresholdPercentage.Key, "0")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.OverloadedMemoryThresholdPercentage.Key)
_, err := suite.loader.requestResource(context.Background())
suite.NoError(err)
})
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 100,
CollectionID: suite.collectionID,
Level: datapb.SegmentLevel_L0,
Deltalogs: []*datapb.FieldBinlog{
{
Binlogs: []*datapb.Binlog{
{LogSize: 10000, MemorySize: 10000},
{LogSize: 12000, MemorySize: 12000},
},
},
},
InsertChannel: fmt.Sprintf("by-dev-rootcoord-dml_0_%dv0", suite.collectionID),
}
suite.Run("l0_segment_deltalog", func() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key, "50")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key)
resource, err := suite.loader.requestResource(context.Background(), loadInfo)
suite.NoError(err)
suite.EqualValues(1100000, resource.Resource.MemorySize)
})
suite.Run("commits_estimated_resource", func() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key, "2")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key)
patchUsedMemory := mockey.Mock(hardware.GetUsedMemoryCount).To(func() uint64 {
return 100
}).Build()
defer patchUsedMemory.UnPatch()
patchTotalMemory := mockey.Mock(hardware.GetMemoryCount).To(func() uint64 {
return 1024 * 1024 * 1024
}).Build()
defer patchTotalMemory.UnPatch()
patchCPU := mockey.Mock(hardware.GetCPUNum).To(func() int {
return 8
}).Build()
defer patchCPU.UnPatch()
suite.loader.duf.usage.Store(200)
suite.loader.committedResource = LoadResource{
MemorySize: 10,
DiskSize: 20,
}
resource, err := suite.loader.requestResource(context.Background(), loadInfo)
suite.NoError(err)
suite.EqualValues(44000, resource.Resource.MemorySize)
suite.EqualValues(10, resource.CommittedResource.MemorySize)
suite.EqualValues(20, resource.CommittedResource.DiskSize)
suite.Equal(1, resource.ConcurrencyLevel)
suite.EqualValues(44010, suite.loader.committedResource.MemorySize)
suite.EqualValues(20, suite.loader.committedResource.DiskSize)
})
suite.Run("estimate_failed", func() {
invalidCollectionID := suite.collectionID + 1
invalidSchema := &schemapb.CollectionSchema{
Name: "invalid",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 100, Name: "duplicated", DataType: schemapb.DataType_Int64},
},
}
originalCollectionManager := suite.manager.Collection
collectionManager := NewMockCollectionManager(suite.T())
collectionManager.EXPECT().Get(invalidCollectionID).
Return(NewCollectionWithoutSegcoreForTest(invalidCollectionID, invalidSchema))
suite.manager.Collection = collectionManager
defer func() {
suite.manager.Collection = originalCollectionManager
}()
_, err := suite.loader.requestResource(context.Background(), &querypb.SegmentLoadInfo{
SegmentID: 101,
CollectionID: invalidCollectionID,
})
suite.Error(err)
})
}
func (suite *SegmentLoaderDetailSuite) TestEstimateSegmentLoadingResourceUsage() {
ctx := context.Background()
emptyUsage, maxSegmentSize, err := suite.loader.estimateSegmentLoadingResourceUsage(ctx)
suite.NoError(err)
suite.Zero(emptyUsage.MemorySize)
suite.Zero(emptyUsage.DiskSize)
suite.Zero(maxSegmentSize)
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key, "2")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.Key)
loadInfo1 := &querypb.SegmentLoadInfo{
SegmentID: 100,
CollectionID: suite.collectionID,
Level: datapb.SegmentLevel_L0,
Deltalogs: []*datapb.FieldBinlog{
{
Binlogs: []*datapb.Binlog{
{LogSize: 10, MemorySize: 10},
},
},
},
}
loadInfo2 := &querypb.SegmentLoadInfo{
SegmentID: 101,
CollectionID: suite.collectionID,
Level: datapb.SegmentLevel_L0,
Deltalogs: []*datapb.FieldBinlog{
{
Binlogs: []*datapb.Binlog{
{LogSize: 30, MemorySize: 30},
},
},
},
}
usage, maxSegmentSize, err := suite.loader.estimateSegmentLoadingResourceUsage(ctx, loadInfo1, loadInfo2)
suite.NoError(err)
suite.EqualValues(80, usage.MemorySize)
suite.Zero(usage.DiskSize)
suite.Zero(usage.MmapFieldCount)
suite.Empty(usage.FieldGpuMemorySize)
suite.EqualValues(60, maxSegmentSize)
}
func (suite *SegmentLoaderDetailSuite) TestCheckLoadingResourceWithGpuLimit() {
ctx := context.Background()
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.Key)
patchGpu := mockey.Mock(hardware.GetAllGPUMemoryInfo).To(func() ([]hardware.GPUMemoryInfo, error) {
return []hardware.GPUMemoryInfo{{TotalMemory: 100, FreeMemory: 100}}, nil
}).Build()
defer patchGpu.UnPatch()
err := suite.loader.checkLoadingResource(ctx, mlog.With(), &ResourceUsage{
MemorySize: 10,
FieldGpuMemorySize: []uint64{100},
}, 10, 1000, 100, 0)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrSegmentRequestResourceFailed))
}
func (suite *SegmentLoaderDetailSuite) TestCheckLoadingResourceReleasesTieredReservationOnGpuLimit() {
ctx := context.Background()
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.Key, "true")
suite.Require().NoError(initcore.InitTieredStorage(paramtable.Get()))
defer func() {
paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.Key)
suite.Require().NoError(initcore.InitTieredStorage(paramtable.Get()))
}()
patchGpu := mockey.Mock(hardware.GetAllGPUMemoryInfo).To(func() ([]hardware.GPUMemoryInfo, error) {
return []hardware.GPUMemoryInfo{{TotalMemory: 100, FreeMemory: 100}}, nil
}).Build()
defer patchGpu.UnPatch()
err := suite.loader.checkLoadingResource(ctx, mlog.With(), &ResourceUsage{
MemorySize: 10,
DiskSize: 10,
FieldGpuMemorySize: []uint64{100},
}, 10, 1000, 100, 0)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrSegmentRequestResourceFailed))
}
func (suite *SegmentLoaderDetailSuite) TestCheckLoadingResourceWithDiskLimit() {
ctx := context.Background()
// Save original value and restore after test
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.DiskCapacityLimit.Key, "1") // 1MB
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.DiskCapacityLimit.Key)
// Set disk usage threshold
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MaxDiskUsagePercentage.Key, "0.8") // 80% threshold
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MaxDiskUsagePercentage.Key)
// set mmap, trigger dist cost
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
// Create a test segment that would exceed the disk limit
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 1000,
BinlogPaths: []*datapb.FieldBinlog{
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{
LogPath: "test_path",
LogSize: 1024 * 1024 * 1024 * 2, // 2GB
MemorySize: 1024 * 1024 * 1024 * 4,
},
},
},
{
FieldID: 105,
Binlogs: []*datapb.Binlog{
{
LogPath: "test_path",
LogSize: 1024 * 1024 * 1024 * 2, // 2GB
MemorySize: 1024 * 1024 * 1024 * 4,
},
},
},
},
}
memUsage := uint64(100 * 1024 * 1024) // 100MB
totalMem := uint64(1024 * 1024 * 1024) // 1GB
localDiskUsage := int64(100 * 1024) // 100KB
loadingUsage, maxSegmentSize, err := suite.loader.estimateSegmentLoadingResourceUsage(ctx, loadInfo)
suite.NoError(err)
err = suite.loader.checkLoadingResource(ctx, mlog.With(), loadingUsage, maxSegmentSize, totalMem, memUsage, localDiskUsage)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrSegmentRequestResourceFailed))
}
func (suite *SegmentLoaderDetailSuite) TestCheckLoadingResourceWithMemoryLimit() {
ctx := context.Background()
// Create a test segment that would exceed the memory limit
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: suite.segmentID,
PartitionID: suite.partitionID,
CollectionID: suite.collectionID,
NumOfRows: 1000,
BinlogPaths: []*datapb.FieldBinlog{
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{
LogPath: "test_path",
LogSize: 1024 * 1024, // 1MB
MemorySize: 1024 * 1024 * 900, // 900MB
},
},
},
},
}
memUsage := uint64(100 * 1024 * 1024) // 100MB
totalMem := uint64(1024 * 1024 * 1024) // 1GB
localDiskUsage := int64(100 * 1024) // 100KB
// Set memory threshold to 80%
paramtable.Get().Save("queryNode.overloadedMemoryThresholdPercentage", "0.8")
defer paramtable.Get().Reset("queryNode.overloadedMemoryThresholdPercentage")
loadingUsage, maxSegmentSize, err := suite.loader.estimateSegmentLoadingResourceUsage(ctx, loadInfo)
suite.NoError(err)
err = suite.loader.checkLoadingResource(ctx, mlog.With(), loadingUsage, maxSegmentSize, totalMem, memUsage, localDiskUsage)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrSegmentRequestResourceFailed))
}
func (suite *SegmentLoaderDetailSuite) TestCheckSegmentGpuMemSizeWithBatchedEstimates() {
patch := mockey.Mock(hardware.GetAllGPUMemoryInfo).To(func() ([]hardware.GPUMemoryInfo, error) {
return []hardware.GPUMemoryInfo{{TotalMemory: 100, FreeMemory: 100}}, nil
}).Build()
defer patch.UnPatch()
err := checkSegmentGpuMemSize([]uint64{30, 30, 30}, 1.0)
suite.NoError(err)
}
// SegmentLoaderTextIndexEstimateSuite tests resource estimation for text index (TextStatsLogs).
// These tests directly call estimateLoadingResourceUsageOfSegment and
// estimateLogicalResourceUsageOfSegment to verify that TextStatsLogs are correctly
// accounted for in both physical loading and logical resource estimates.
type SegmentLoaderTextIndexEstimateSuite struct {
suite.Suite
schema *schemapb.CollectionSchema
}
func (suite *SegmentLoaderTextIndexEstimateSuite) SetupSuite() {
paramtable.Init()
// Minimal schema: just a PK field + one VarChar field; no need for full schema
// since text stats estimation only iterates loadInfo.GetTextStatsLogs() directly.
suite.schema = &schemapb.CollectionSchema{
Name: "test_text_estimate",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar},
{FieldID: 102, Name: "text2", DataType: schemapb.DataType_VarChar},
},
}
}
// baseLoadInfo returns a minimal SegmentLoadInfo with the given TextStatsLogs.
func (suite *SegmentLoaderTextIndexEstimateSuite) baseLoadInfo(textStats map[int64]*datapb.TextIndexStats) *querypb.SegmentLoadInfo {
return &querypb.SegmentLoadInfo{
SegmentID: 1,
PartitionID: 2,
CollectionID: 3,
NumOfRows: 100,
TextStatsLogs: textStats,
}
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_NonMmap_NoTieredEviction() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(50 * 1024 * 1024) // 50 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: false,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
jsonKeyStatsExpansionFactor: 1.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(textIndexSize, usage.MemorySize, "non-mmap text index must be counted in memory")
suite.EqualValues(0, usage.DiskSize, "non-mmap text index must not be counted in disk")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_Mmap_NoTieredEviction() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(50 * 1024 * 1024) // 50 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: false,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
jsonKeyStatsExpansionFactor: 1.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize, "mmap text index must not be counted in memory")
suite.EqualValues(textIndexSize, usage.DiskSize, "mmap text index must be counted in disk")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_TieredEvictionEnabled_TextIndexSkipped() {
// When tiered eviction is enabled the caching layer manages text indexes,
// so they must NOT be added to the physical loading estimate.
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(50 * 1024 * 1024)
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize, "text index must be skipped when tiered eviction is enabled")
suite.EqualValues(0, usage.DiskSize)
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_MultipleTextFields() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const size1 = int64(30 * 1024 * 1024) // 30 MB
const size2 = int64(20 * 1024 * 1024) // 20 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: size1},
102: {FieldID: 102, MemorySize: size2},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: false,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(size1+size2, usage.MemorySize, "all text index sizes must be summed")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_ExpansionFactor() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(40 * 1024 * 1024) // 40 MB
const expansionFactor = 1.5
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: false,
textIndexExpansionFactor: expansionFactor,
deltaDataExpansionFactor: 2.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
expected := uint64(float64(textIndexSize) * expansionFactor)
suite.EqualValues(expected, usage.MemorySize, "expansion factor must be applied to text index size")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_EmptyTextStats_NoContribution() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
loadInfo := suite.baseLoadInfo(nil) // no TextStatsLogs
factor := resourceEstimateFactor{
TieredEvictionEnabled: false,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize)
suite.EqualValues(0, usage.DiskSize)
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLoadingEstimate_Mmap_TieredEvictionEnabled_TextIndexSkipped() {
// When tiered eviction is enabled, text index is managed by caching layer
// regardless of mmap setting — must NOT appear in physical loading estimate.
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(50 * 1024 * 1024)
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize, "text index must be skipped when tiered eviction is enabled (mmap)")
suite.EqualValues(0, usage.DiskSize, "text index must be skipped when tiered eviction is enabled (mmap)")
}
// --- estimateLogicalResourceUsageOfSegment tests ---
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_NonMmap_EvictableMemory() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(60 * 1024 * 1024) // 60 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0, // 100% of evictable memory is counted
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(textIndexSize, usage.MemorySize, "non-mmap text index must be in evictable memory")
suite.EqualValues(0, usage.DiskSize)
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_Mmap_EvictableDisk() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(60 * 1024 * 1024) // 60 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0,
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize, "mmap text index must not be in memory")
suite.EqualValues(textIndexSize, usage.DiskSize, "mmap text index must be in evictable disk")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_CacheRatioApplied() {
// Verify TieredEvictableMemoryCacheRatio is applied to the evictable text index size.
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(100 * 1024 * 1024) // 100 MB
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
const cacheRatio = 0.5
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: cacheRatio,
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
expected := uint64(float64(textIndexSize) * cacheRatio)
suite.EqualValues(expected, usage.MemorySize, "cache ratio must be applied to evictable text index memory")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_EmptyTextStats_NoContribution() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
loadInfo := suite.baseLoadInfo(nil) // no TextStatsLogs
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0,
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize)
suite.EqualValues(0, usage.DiskSize)
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_MultipleTextFields() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const size1 = int64(30 * 1024 * 1024)
const size2 = int64(20 * 1024 * 1024)
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: size1},
102: {FieldID: 102, MemorySize: size2},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0,
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(size1+size2, usage.MemorySize, "all text index sizes must be summed in logical estimate")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_DiskCacheRatioApplied() {
// Verify TieredEvictableDiskCacheRatio is applied to mmap text index size.
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "true")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(100 * 1024 * 1024)
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
const diskCacheRatio = 0.3
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: 1.0,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0,
TieredEvictableDiskCacheRatio: diskCacheRatio,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.EqualValues(0, usage.MemorySize, "mmap text index must not be in memory")
expected := uint64(float64(textIndexSize) * diskCacheRatio)
suite.EqualValues(expected, usage.DiskSize, "disk cache ratio must be applied to mmap text index")
}
func (suite *SegmentLoaderTextIndexEstimateSuite) TestLogicalEstimate_ExpansionFactor() {
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.MmapScalarField.Key, "false")
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.MmapScalarField.Key)
const textIndexSize = int64(40 * 1024 * 1024)
const expansionFactor = 2.0
loadInfo := suite.baseLoadInfo(map[int64]*datapb.TextIndexStats{
101: {FieldID: 101, MemorySize: textIndexSize},
})
factor := resourceEstimateFactor{
TieredEvictionEnabled: true,
textIndexExpansionFactor: expansionFactor,
deltaDataExpansionFactor: 2.0,
TieredEvictableMemoryCacheRatio: 1.0,
TieredEvictableDiskCacheRatio: 1.0,
}
usage, err := estimateLogicalResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
expected := uint64(float64(textIndexSize) * expansionFactor)
suite.EqualValues(expected, usage.MemorySize)
}
func TestSeparateLoadInfoV2_ExternalFieldIndexNotSkipped(t *testing.T) {
// Verifies that indexes on external fields are NOT skipped in separateLoadInfoV2.
// Previously, external field indexes were filtered out, preventing index loading for external tables.
schema := &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{Name: "__virtual_pk__", FieldID: 100, DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{Name: "id", FieldID: 101, DataType: schemapb.DataType_Int64, ExternalField: "id"},
{Name: "embedding", FieldID: 102, DataType: schemapb.DataType_FloatVector, ExternalField: "embedding"},
},
}
loadInfo := &querypb.SegmentLoadInfo{
StorageVersion: storage.StorageV3,
IndexInfos: []*querypb.FieldIndexInfo{
{
FieldID: 101,
IndexID: 1001,
IndexFilePaths: []string{"index/101/file1", "index/101/file2"},
},
{
FieldID: 102,
IndexID: 1002,
IndexFilePaths: []string{"index/102/file1"},
},
},
// External segments have no BinlogPaths — data is loaded from manifest
BinlogPaths: []*datapb.FieldBinlog{},
}
indexedFieldInfos, fieldBinlogs, _, _, _, _, _ := separateLoadInfoV2(loadInfo, schema)
// External field indexes should be included (not skipped)
assert.Len(t, indexedFieldInfos, 0, "no binlog paths means no indexed field infos via binlog matching")
assert.Len(t, fieldBinlogs, 0, "no binlog paths for external segments")
// The key point: fieldID2IndexInfo should contain external field indexes.
// Even though indexedFieldInfos is empty (no binlog to match), the indexes are
// still available in loadInfo.IndexInfos for the C++ managed loading path.
// This test ensures the code doesn't filter them out from IndexInfos.
assert.Len(t, loadInfo.IndexInfos, 2, "IndexInfos should still contain all external field indexes")
}
func TestSeparateLoadInfoV2_MixedExternalAndNormalFields(t *testing.T) {
// Verify that normal field indexes are matched while external field binlogs are skipped.
// Index info for both external and normal fields should remain in IndexInfos.
schema := &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{Name: "__virtual_pk__", FieldID: 100, DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{Name: "id", FieldID: 101, DataType: schemapb.DataType_Int64, ExternalField: "id"},
{Name: "normal_field", FieldID: 103, DataType: schemapb.DataType_Int64},
},
}
loadInfo := &querypb.SegmentLoadInfo{
StorageVersion: storage.StorageV3,
IndexInfos: []*querypb.FieldIndexInfo{
{
FieldID: 101,
IndexID: 1001,
IndexFilePaths: []string{"index/101/file1"},
},
{
FieldID: 103,
IndexID: 1003,
IndexFilePaths: []string{"index/103/file1"},
},
},
BinlogPaths: []*datapb.FieldBinlog{
// Normal field has binlog; external field 101 does not
{FieldID: 103, Binlogs: []*datapb.Binlog{{LogPath: "binlog/103"}}},
},
}
indexedFieldInfos, fieldBinlogs, _, _, _, _, _ := separateLoadInfoV2(loadInfo, schema)
// Normal field 103 has binlog + index → indexed
assert.Len(t, indexedFieldInfos, 1)
assert.Contains(t, indexedFieldInfos, int64(1003))
assert.Len(t, fieldBinlogs, 0)
// IndexInfos still contains both external and normal field indexes
assert.Len(t, loadInfo.IndexInfos, 2)
}
// ExternalSegmentEstimateSuite tests resource estimation for external table segments
// with fake binlogs (pre-computed MemorySize from DataNode Take sampling).
type ExternalSegmentEstimateSuite struct {
suite.Suite
schema *schemapb.CollectionSchema
}
func (suite *ExternalSegmentEstimateSuite) SetupSuite() {
paramtable.Init()
suite.schema = &schemapb.CollectionSchema{
Name: "test_external_estimate",
ExternalSource: "s3://bucket/data/",
ExternalSpec: `{"format":"parquet"}`,
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "__virtual_pk__", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
{
FieldID: 102, Name: "vec", DataType: schemapb.DataType_FloatVector, ExternalField: "vec_col",
TypeParams: []*commonpb.KeyValuePair{{Key: common.DimKey, Value: "128"}},
},
},
}
}
func (suite *ExternalSegmentEstimateSuite) externalLoadInfo(numRows int64, memorySize int64) *querypb.SegmentLoadInfo {
return &querypb.SegmentLoadInfo{
SegmentID: 1,
CollectionID: 100,
NumOfRows: numRows,
StorageVersion: storage.StorageV3,
ManifestPath: `{"ver":1,"base_path":"external/100/segments/1"}`,
BinlogPaths: []*datapb.FieldBinlog{
{
FieldID: 0, // DefaultShortColumnGroupID
ChildFields: []int64{100, 101, 102},
Binlogs: []*datapb.Binlog{
{LogID: 1, EntriesNum: numRows, MemorySize: memorySize, LogSize: memorySize},
},
},
},
}
}
func (suite *ExternalSegmentEstimateSuite) TestEstimatedBytesPerRow() {
loadInfo := suite.externalLoadInfo(1000, 576000) // 576 bytes/row
factor := resourceEstimateFactor{externalRawDataFactor: 1.0}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
suite.NotNil(usage)
suite.Equal(int64(576), loadInfo.EstimatedBytesPerRow)
}
func (suite *ExternalSegmentEstimateSuite) TestExternalRawDataFactor() {
loadInfo := suite.externalLoadInfo(1000, 100000)
factor := resourceEstimateFactor{externalRawDataFactor: 1.5}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
// PART 2 adds binlogSize (100000) to memory
// PART 2.5 adds extra = 100000 * (1.5 - 1.0) = 50000
suite.True(usage.MemorySize >= 150000, "should include raw data factor: got %d", usage.MemorySize)
}
func (suite *ExternalSegmentEstimateSuite) TestExternalRawDataFactor_NoExtraWhenFactorLe1() {
loadInfo := suite.externalLoadInfo(1000, 100000)
factor := resourceEstimateFactor{externalRawDataFactor: 0.8}
usage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
// factor <= 1.0, no extra memory added by PART 2.5
suite.True(usage.MemorySize >= 100000, "should include base binlog size")
}
func (suite *ExternalSegmentEstimateSuite) TestLazyLoadSubtractsRawData() {
// Build a separate schema with warmup=disable on all external fields
lazySchema := &schemapb.CollectionSchema{
Name: "test_external_lazy",
ExternalSource: "s3://bucket/data/",
ExternalSpec: `{"format":"parquet"}`,
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "__virtual_pk__", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{
FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col",
TypeParams: []*commonpb.KeyValuePair{{Key: common.WarmupKey, Value: common.WarmupDisable}},
},
{
FieldID: 102, Name: "vec", DataType: schemapb.DataType_FloatVector, ExternalField: "vec_col",
TypeParams: []*commonpb.KeyValuePair{
{Key: common.DimKey, Value: "128"},
{Key: common.WarmupKey, Value: common.WarmupDisable},
},
},
},
}
// Override global warmup defaults to disable
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.TieredWarmupScalarField.Key, common.WarmupDisable)
paramtable.Get().Save(paramtable.Get().QueryNodeCfg.TieredWarmupVectorField.Key, common.WarmupDisable)
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.TieredWarmupScalarField.Key)
defer paramtable.Get().Reset(paramtable.Get().QueryNodeCfg.TieredWarmupVectorField.Key)
suite.True(isExternalCollectionLazyLoad(lazySchema), "schema should be detected as lazy load")
loadInfo := suite.externalLoadInfo(1000, 100000)
factor := resourceEstimateFactor{externalRawDataFactor: 1.0}
// With lazy load, memory estimate should be less than or equal to
// the non-lazy-load case (which is at least binlogSize=100000).
// The exact value depends on PART 2's mmap/tiered logic.
nonLazyUsage, err := estimateLoadingResourceUsageOfSegment(suite.schema, loadInfo, factor)
suite.NoError(err)
lazyUsage, err := estimateLoadingResourceUsageOfSegment(lazySchema, loadInfo, factor)
suite.NoError(err)
suite.True(lazyUsage.MemorySize <= nonLazyUsage.MemorySize,
"lazy load memory (%d) should be <= non-lazy (%d)", lazyUsage.MemorySize, nonLazyUsage.MemorySize)
}
func TestGpuIndexRequiresGpu(t *testing.T) {
tests := []struct {
name string
params []*commonpb.KeyValuePair
expected bool
}{
{
name: "GPU_CAGRA adapt for CPU",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_CAGRA"},
{Key: "adapt_for_cpu", Value: "true"},
},
expected: false,
},
{
name: "GPU_CUVS_CAGRA adapt for CPU",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_CUVS_CAGRA"},
{Key: "adapt_for_cpu", Value: "1"},
},
expected: false,
},
{
name: "GPU_CAGRA without adapt for CPU",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_CAGRA"},
},
expected: true,
},
{
name: "GPU_CAGRA invalid adapt for CPU",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_CAGRA"},
{Key: "adapt_for_cpu", Value: "invalid"},
},
expected: true,
},
{
name: "other GPU index ignores adapt for CPU",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_IVF_FLAT"},
{Key: "adapt_for_cpu", Value: "true"},
},
expected: true,
},
{
name: "CPU index",
params: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "HNSW"},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, gpuIndexRequiresGpu(test.params))
})
}
t.Run("GPU_CAGRA adapt for CPU from load config", func(t *testing.T) {
params := paramtable.Get()
oldEnable := params.KnowhereConfig.Enable.GetValue()
adaptKey := params.KnowhereConfig.IndexParam.KeyPrefix + "GPU_CAGRA.load.adapt_for_cpu"
oldAdaptValue := params.GetWithDefault(adaptKey, "")
defer params.Save(params.KnowhereConfig.Enable.Key, oldEnable)
defer func() {
if oldAdaptValue == "" {
params.Remove(adaptKey)
return
}
params.Save(adaptKey, oldAdaptValue)
}()
params.Save(params.KnowhereConfig.Enable.Key, "true")
params.Save(adaptKey, "true")
assert.False(t, gpuIndexRequiresGpu([]*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "GPU_CAGRA"},
}))
})
}
func TestEstimateLoadingResourceUsage_DroppedFieldSkipped(t *testing.T) {
paramtable.Init()
// Schema only has fieldID=100 and 101; fieldID=999 is "dropped" (not in schema)
schema := &schemapb.CollectionSchema{
Name: "test_dropped_field",
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar},
},
}
t.Run("index on dropped field is skipped", func(t *testing.T) {
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 1,
CollectionID: 10,
NumOfRows: 100,
IndexInfos: []*querypb.FieldIndexInfo{
{
FieldID: 999, // dropped field
IndexID: 2001,
IndexFilePaths: []string{"index/999/file1"},
},
},
}
factor := resourceEstimateFactor{}
usage, err := estimateLoadingResourceUsageOfSegment(schema, loadInfo, factor)
assert.NoError(t, err)
assert.NotNil(t, usage)
})
t.Run("binlog with dropped field in child fields is skipped", func(t *testing.T) {
loadInfo := &querypb.SegmentLoadInfo{
SegmentID: 2,
CollectionID: 10,
NumOfRows: 100,
BinlogPaths: []*datapb.FieldBinlog{
{
FieldID: 888, // column group containing a dropped field
Binlogs: []*datapb.Binlog{
{LogSize: 1024},
},
ChildFields: []int64{999}, // dropped field
},
},
}
factor := resourceEstimateFactor{}
usage, err := estimateLoadingResourceUsageOfSegment(schema, loadInfo, factor)
assert.NoError(t, err)
assert.NotNil(t, usage)
})
}
func TestSegmentLoader(t *testing.T) {
suite.Run(t, &SegmentLoaderSuite{})
suite.Run(t, &SegmentLoaderDetailSuite{})
suite.Run(t, &SegmentLoaderTextIndexEstimateSuite{})
suite.Run(t, &ExternalSegmentEstimateSuite{})
}