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
2678 lines
89 KiB
Go
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{})
|
|
}
|