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
3976 lines
147 KiB
Go
3976 lines
147 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 external
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/apache/arrow/go/v17/arrow"
|
|
"github.com/apache/arrow/go/v17/arrow/array"
|
|
"github.com/apache/arrow/go/v17/arrow/memory"
|
|
"github.com/bytedance/mockey"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"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/storage"
|
|
"github.com/milvus-io/milvus/internal/storagecommon"
|
|
"github.com/milvus-io/milvus/internal/storagev2/packed"
|
|
"github.com/milvus-io/milvus/internal/util/function/embedding"
|
|
"github.com/milvus-io/milvus/internal/util/hookutil"
|
|
"github.com/milvus-io/milvus/pkg/v3/common"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/indexcgopb"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/externalspec"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
type RefreshExternalCollectionTaskSuite struct {
|
|
suite.Suite
|
|
collectionID int64
|
|
taskID int64
|
|
}
|
|
|
|
type fakeRecordReader struct {
|
|
records []storage.Record
|
|
errs []error
|
|
idx int
|
|
}
|
|
|
|
func (r *fakeRecordReader) Next() (storage.Record, error) {
|
|
if r.idx < len(r.errs) && r.errs[r.idx] != nil {
|
|
err := r.errs[r.idx]
|
|
r.idx++
|
|
return nil, err
|
|
}
|
|
if r.idx >= len(r.records) {
|
|
return nil, io.EOF
|
|
}
|
|
record := r.records[r.idx]
|
|
r.idx++
|
|
return record, nil
|
|
}
|
|
|
|
func (r *fakeRecordReader) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) SetupSuite() {
|
|
s.collectionID = 1000
|
|
s.taskID = 1
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestNewRefreshExternalCollectionTask() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
ExternalSpec: "test_spec",
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
s.NotNil(task)
|
|
s.Equal(s.collectionID, task.req.GetCollectionID())
|
|
s.Equal(s.taskID, task.req.GetTaskID())
|
|
s.Equal(indexpb.JobState_JobStateInit, task.GetState())
|
|
s.Contains(task.Name(), "RefreshExternalCollectionTask")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestTaskLifecycle() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{Name: "id", ExternalField: "id"},
|
|
},
|
|
},
|
|
StorageConfig: &indexpb.StorageConfig{
|
|
StorageType: "local",
|
|
},
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{ID: 1, CollectionID: s.collectionID, NumOfRows: 1000},
|
|
{ID: 2, CollectionID: s.collectionID, NumOfRows: 2000},
|
|
},
|
|
PreAllocatedSegmentIds: &datapb.IDRange{
|
|
Begin: 1000,
|
|
End: 2000,
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// Test OnEnqueue
|
|
err := task.OnEnqueue(ctx)
|
|
s.NoError(err)
|
|
|
|
// Test PreExecute - validates schema, storage config, external source
|
|
err = task.PreExecute(ctx)
|
|
s.NoError(err)
|
|
s.NotNil(task.parsedSpec)
|
|
s.Equal("parquet", task.parsedSpec.Format)
|
|
s.Equal([]string{"id"}, task.columns)
|
|
|
|
// Test PostExecute (Execute skipped as it requires CGO FFI calls)
|
|
err = task.PostExecute(ctx)
|
|
s.NoError(err)
|
|
|
|
// Test GetSlot
|
|
s.Equal(int64(1), task.GetSlot())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecuteClonesSchemaBeforeFillingExternalMetadata() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
sourceSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{Name: "id", ExternalField: "id"},
|
|
},
|
|
}
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: sourceSchema,
|
|
StorageConfig: &indexpb.StorageConfig{
|
|
StorageType: "local",
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
|
|
s.NoError(err)
|
|
s.Empty(sourceSchema.GetExternalSource())
|
|
s.Empty(sourceSchema.GetExternalSpec())
|
|
s.NotSame(sourceSchema, task.req.GetSchema())
|
|
s.Equal(req.GetExternalSource(), task.req.GetSchema().GetExternalSource())
|
|
s.Equal(req.GetExternalSpec(), task.req.GetSchema().GetExternalSpec())
|
|
s.Equal([]string{"id"}, task.columns)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecuteWithNilRequest() {
|
|
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // cancel is deferred below
|
|
defer cancel()
|
|
task := &RefreshExternalCollectionTask{
|
|
ctx: ctx,
|
|
req: nil,
|
|
}
|
|
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSetAndGetState() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
task.SetState(indexpb.JobState_JobStateInProgress, "")
|
|
s.Equal(indexpb.JobState_JobStateInProgress, task.GetState())
|
|
|
|
task.SetState(indexpb.JobState_JobStateFailed, "test failure")
|
|
s.Equal(indexpb.JobState_JobStateFailed, task.GetState())
|
|
s.Equal("test failure", task.failReason)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestReset() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.Reset()
|
|
|
|
s.Nil(task.ctx)
|
|
s.Nil(task.req)
|
|
s.Nil(task.tr)
|
|
s.Nil(task.updatedSegments)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_Empty() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), []packed.Fragment{})
|
|
s.NoError(err)
|
|
s.Nil(result)
|
|
}
|
|
|
|
// Regression for #49225: zero-row parquet produces fragments whose RowCount
|
|
// sums to 0 and previously triggered divide-by-zero panic in
|
|
// balanceFragmentsToSegments. Must return error, never panic.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ZeroTotalRowsWithFragments() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 0, FilePath: "s3://bucket/zero.parquet"},
|
|
{FragmentID: 2, RowCount: 0, FilePath: "s3://bucket/zero2.parquet"},
|
|
}
|
|
s.NotPanics(func() {
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
s.Contains(err.Error(), "zero total rows")
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_SingleFragment() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 64, "vec_col": 512}, nil).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.Equal(int64(500), result[0].GetNumOfRows())
|
|
s.Equal(storage.StorageV3, result[0].GetStorageVersion(),
|
|
"external segments should have StorageVersion=V3")
|
|
|
|
// Verify fake binlogs
|
|
s.Len(result[0].GetBinlogs(), 1)
|
|
fb := result[0].GetBinlogs()[0]
|
|
s.Equal(int64(0), fb.GetFieldID(), "should use DefaultShortColumnGroupID")
|
|
s.ElementsMatch([]int64{100, 101}, fb.GetChildFields())
|
|
s.Len(fb.GetBinlogs(), 1)
|
|
binlog := fb.GetBinlogs()[0]
|
|
s.Equal(int64(500), binlog.GetEntriesNum())
|
|
// avgBytesPerRow = 64 + 512 = 576, memorySize = 576 * 500 = 288000
|
|
s.Equal(int64(288000), binlog.GetMemorySize())
|
|
s.Equal(int64(288000), binlog.GetLogSize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_MultipleFragments() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 100}, nil).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 300000},
|
|
{FragmentID: 2, RowCount: 400000},
|
|
{FragmentID: 3, RowCount: 500000},
|
|
{FragmentID: 4, RowCount: 600000},
|
|
{FragmentID: 5, RowCount: 200000},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
|
|
// Verify all segments have StorageVersion=V3 and fake binlogs
|
|
for i, seg := range result {
|
|
s.Equal(storage.StorageV3, seg.GetStorageVersion(),
|
|
"segment %d should have StorageVersion=V3", i)
|
|
s.NotEmpty(seg.GetBinlogs(), "segment %d should have fake binlogs", i)
|
|
fb := seg.GetBinlogs()[0]
|
|
s.Equal(int64(0), fb.GetFieldID())
|
|
s.Equal([]int64{100}, fb.GetChildFields())
|
|
// MemorySize = avgBytesPerRow(100) * numRows
|
|
s.Equal(int64(100)*seg.GetNumOfRows(), fb.GetBinlogs()[0].GetMemorySize())
|
|
}
|
|
|
|
// Verify total rows are preserved
|
|
var totalRows int64
|
|
for _, seg := range result {
|
|
totalRows += seg.GetNumOfRows()
|
|
}
|
|
s.Equal(int64(2000000), totalRows)
|
|
|
|
// Verify segments are reasonably balanced
|
|
if len(result) > 1 {
|
|
minRows, maxRows := result[0].GetNumOfRows(), result[0].GetNumOfRows()
|
|
for _, seg := range result {
|
|
if seg.GetNumOfRows() < minRows {
|
|
minRows = seg.GetNumOfRows()
|
|
}
|
|
if seg.GetNumOfRows() > maxRows {
|
|
maxRows = seg.GetNumOfRows()
|
|
}
|
|
}
|
|
// The difference between max and min should be reasonable
|
|
avgFragmentSize := int64(2000000 / 5)
|
|
s.Less(maxRows-minRows, avgFragmentSize*2)
|
|
}
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecuteContextCanceled() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
cancel()
|
|
|
|
err := task.PreExecute(ctx)
|
|
s.ErrorIs(err, context.Canceled)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteContextCanceled() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
cancel()
|
|
|
|
err := task.Execute(ctx)
|
|
s.ErrorIs(err, context.Canceled)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegmentsContextCanceled() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
cancel()
|
|
|
|
result, err := task.balanceFragmentsToSegments(ctx, []packed.Fragment{{FragmentID: 1, RowCount: 10}})
|
|
s.ErrorIs(err, context.Canceled)
|
|
s.Nil(result)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_AllFragmentsExist() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{ID: 1, CollectionID: s.collectionID, NumOfRows: 1000},
|
|
{ID: 2, CollectionID: s.collectionID, NumOfRows: 2000},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// Simulate current segment fragments mapping (use FilePath as identifier)
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", RowCount: 1000}},
|
|
2: []packed.Fragment{{FragmentID: 102, FilePath: "/data/file2.parquet", RowCount: 2000}},
|
|
}
|
|
|
|
// New fragments contain all existing fragments
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 101, FilePath: "/data/file1.parquet", RowCount: 1000},
|
|
{FragmentID: 102, FilePath: "/data/file2.parquet", RowCount: 2000},
|
|
}
|
|
|
|
result, err := task.organizeSegments(context.Background(), currentSegmentFragments, newFragments)
|
|
s.NoError(err)
|
|
|
|
// Both segments should be kept
|
|
s.Len(result, 2)
|
|
|
|
// Verify kept/new tracking
|
|
s.ElementsMatch([]int64{1, 2}, task.GetKeptSegmentIDs())
|
|
s.Empty(task.GetUpdatedSegments())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_SameFragmentsMissingFieldPatchesSegment() {
|
|
paramtable.Init()
|
|
|
|
ctx := context.Background()
|
|
partitionID := int64(2000)
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
TaskID: s.taskID,
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
ExternalSource: "s3://bucket/data/",
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Version: 4,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", ExternalField: "id"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec"},
|
|
{FieldID: 102, Name: "text", ExternalField: "text"},
|
|
{FieldID: 103, Name: "score", ExternalField: "score"},
|
|
},
|
|
},
|
|
CurrentSegments: []*datapb.SegmentInfo{{
|
|
ID: 10,
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
NumOfRows: 1000,
|
|
ManifestPath: `{"base_path":"seg10","ver":1}`,
|
|
StorageVersion: storage.StorageV3,
|
|
SchemaVersion: 3,
|
|
Binlogs: buildFakeBinlogs(10, 1000, 3000, &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", ExternalField: "id"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec"},
|
|
{FieldID: 102, Name: "text", ExternalField: "text"},
|
|
},
|
|
}, "parquet"),
|
|
}},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"id", "vec", "text", "score"}
|
|
|
|
mockAppend := mockey.Mock(packed.AppendSegmentManifestColumns).
|
|
To(func(ctx context.Context, oldManifestPath string, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig) (string, error) {
|
|
s.Equal(`{"base_path":"seg10","ver":1}`, oldManifestPath)
|
|
s.Equal([]string{"score"}, columns)
|
|
return `{"base_path":"seg10","ver":2}`, nil
|
|
}).Build()
|
|
defer mockAppend.UnPatch()
|
|
|
|
mockSample := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
To(func(manifestPath string, sampleRows int, collectionID int64, externalSource string, externalSpec string, schema *schemapb.CollectionSchema, storageConfig *indexpb.StorageConfig) (map[string]int64, error) {
|
|
s.Equal(`{"base_path":"seg10","ver":2}`, manifestPath)
|
|
s.Equal(100, sampleRows)
|
|
s.Equal(s.collectionID, collectionID)
|
|
s.Equal("s3://bucket/data/", externalSource)
|
|
s.Equal(`{"format":"parquet"}`, externalSpec)
|
|
s.Equal(req.GetSchema(), schema)
|
|
s.Equal(req.GetStorageConfig(), storageConfig)
|
|
return map[string]int64{"id": 8, "vec": 128, "text": 32, "score": 8}, nil
|
|
}).Build()
|
|
defer mockSample.UnPatch()
|
|
|
|
current := packed.SegmentFragments{
|
|
10: []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}},
|
|
}
|
|
next := []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}}
|
|
|
|
result, err := task.organizeSegments(ctx, current, next)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.Empty(task.GetKeptSegmentIDs())
|
|
s.Len(task.GetUpdatedSegments(), 1)
|
|
patched := task.GetUpdatedSegments()[0]
|
|
s.Equal(int64(10), patched.GetID())
|
|
s.Equal(`{"base_path":"seg10","ver":2}`, patched.GetManifestPath())
|
|
s.Equal(int32(4), patched.GetSchemaVersion())
|
|
s.ElementsMatch([]int64{100, 101, 102, 103}, patched.GetBinlogs()[0].GetChildFields())
|
|
|
|
result, err = task.organizeSegments(ctx, current, next)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.Empty(task.GetKeptSegmentIDs())
|
|
s.Len(task.GetUpdatedSegments(), 1)
|
|
s.Equal(int64(10), task.GetUpdatedSegments()[0].GetID())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_PatchedSegmentCountsFunctionOutputMemory() {
|
|
paramtable.Init()
|
|
|
|
ctx := context.Background()
|
|
partitionID := int64(2000)
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
TaskID: s.taskID,
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
ExternalSource: "s3://bucket/data/",
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Version: 4,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64, ExternalField: "id"},
|
|
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text"},
|
|
{FieldID: 102, Name: "score", DataType: schemapb.DataType_Double, ExternalField: "score"},
|
|
{
|
|
FieldID: 103,
|
|
Name: "embedding",
|
|
DataType: schemapb.DataType_FloatVector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: common.DimKey, Value: "4"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentSegments: []*datapb.SegmentInfo{{
|
|
ID: 10,
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
NumOfRows: 100,
|
|
ManifestPath: `{"base_path":"seg10","ver":1}`,
|
|
StorageVersion: storage.StorageV3,
|
|
SchemaVersion: 3,
|
|
Binlogs: buildFakeBinlogs(10, 100, 4800, &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", DataType: schemapb.DataType_Int64, ExternalField: "id"},
|
|
{FieldID: 101, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text"},
|
|
{
|
|
FieldID: 103,
|
|
Name: "embedding",
|
|
DataType: schemapb.DataType_FloatVector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: common.DimKey, Value: "4"}},
|
|
},
|
|
},
|
|
}, "parquet"),
|
|
}},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"id", "text", "score"}
|
|
|
|
mockAppend := mockey.Mock(packed.AppendSegmentManifestColumns).
|
|
To(func(ctx context.Context, oldManifestPath string, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig) (string, error) {
|
|
s.Equal(`{"base_path":"seg10","ver":1}`, oldManifestPath)
|
|
s.Equal([]string{"score"}, columns)
|
|
return `{"base_path":"seg10","ver":2}`, nil
|
|
}).Build()
|
|
defer mockAppend.UnPatch()
|
|
|
|
mockSample := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"id": 8, "text": 32, "score": 8}, nil).Build()
|
|
defer mockSample.UnPatch()
|
|
|
|
current := packed.SegmentFragments{
|
|
10: []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 100, RowCount: 100}},
|
|
}
|
|
next := []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 100, RowCount: 100}}
|
|
|
|
result, err := task.organizeSegments(ctx, current, next)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
patched := task.GetUpdatedSegments()[0]
|
|
s.ElementsMatch([]int64{100, 101, 102, 103}, patched.GetBinlogs()[0].GetChildFields())
|
|
s.Equal(int64((8+32+8+16)*100), patched.GetBinlogs()[0].GetBinlogs()[0].GetMemorySize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_SameFragmentsAllFieldsCoveredKeepsSegment() {
|
|
ctx := context.Background()
|
|
partitionID := int64(2000)
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
TaskID: s.taskID,
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Version: 4,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", ExternalField: "id"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec"},
|
|
},
|
|
},
|
|
CurrentSegments: []*datapb.SegmentInfo{{
|
|
ID: 10,
|
|
CollectionID: s.collectionID,
|
|
PartitionID: partitionID,
|
|
NumOfRows: 100,
|
|
ManifestPath: `{"base_path":"seg10","ver":1}`,
|
|
StorageVersion: storage.StorageV3,
|
|
SchemaVersion: 4,
|
|
Binlogs: buildFakeBinlogs(10, 100, 3000, &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", ExternalField: "id"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec"},
|
|
},
|
|
}, "parquet"),
|
|
}},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
current := packed.SegmentFragments{
|
|
10: []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 100, RowCount: 100}},
|
|
}
|
|
next := []packed.Fragment{{FilePath: "s3://bucket/data/a.parquet", StartRow: 0, EndRow: 100, RowCount: 100}}
|
|
|
|
result, err := task.organizeSegments(ctx, current, next)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.ElementsMatch([]int64{10}, task.GetKeptSegmentIDs())
|
|
s.Empty(task.GetUpdatedSegments())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_FragmentRemoved() {
|
|
s.T().Skip("Skip test that requires CGO FFI calls for manifest creation")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_PartialFragmentRemoved() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{ID: 1, CollectionID: s.collectionID, NumOfRows: 1000},
|
|
{ID: 2, CollectionID: s.collectionID, NumOfRows: 2000},
|
|
{ID: 3, CollectionID: s.collectionID, NumOfRows: 1500},
|
|
},
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// S1 has file1, S2 has file2, S3 has file3
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", RowCount: 1000}},
|
|
2: []packed.Fragment{{FragmentID: 102, FilePath: "/data/file2.parquet", RowCount: 2000}},
|
|
3: []packed.Fragment{{FragmentID: 103, FilePath: "/data/file3.parquet", RowCount: 1500}},
|
|
}
|
|
|
|
// New fragments: file1 and file3 still exist, file2 removed
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 201, FilePath: "/data/file1.parquet", RowCount: 1000},
|
|
{FragmentID: 203, FilePath: "/data/file3.parquet", RowCount: 1500},
|
|
}
|
|
|
|
result, err := task.organizeSegments(context.Background(), currentSegmentFragments, newFragments)
|
|
s.NoError(err)
|
|
|
|
// S1 and S3 should be kept, S2 should be invalidated
|
|
// No new segments because there are no orphan fragments (all new fragments match kept segments)
|
|
s.Len(result, 2)
|
|
s.ElementsMatch([]int64{1, 3}, task.GetKeptSegmentIDs())
|
|
s.Empty(task.GetUpdatedSegments(), "No orphan fragments to create new segments from")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_NewFragmentsUseBalance() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{ID: 1, CollectionID: s.collectionID, NumOfRows: 1000},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}},
|
|
}
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 201, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000},
|
|
{FragmentID: 202, FilePath: "/data/file2.parquet", StartRow: 0, EndRow: 500, RowCount: 500},
|
|
}
|
|
created := []*datapb.SegmentInfo{{ID: 2, CollectionID: s.collectionID, NumOfRows: 500}}
|
|
|
|
var gotOrphans []packed.Fragment
|
|
mockBalance := mockey.Mock(mockey.GetMethod(task, "balanceFragmentsToSegments")).
|
|
To(func(ctx context.Context, fragments []packed.Fragment) ([]*datapb.SegmentInfo, error) {
|
|
gotOrphans = fragments
|
|
return created, nil
|
|
}).Build()
|
|
defer mockBalance.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, currentSegmentFragments, newFragments)
|
|
s.NoError(err)
|
|
s.ElementsMatch([]int64{1}, task.GetKeptSegmentIDs())
|
|
s.Equal(created, task.GetUpdatedSegments())
|
|
s.Equal([]*datapb.SegmentInfo{req.GetCurrentSegments()[0], created[0]}, result)
|
|
s.Require().Len(gotOrphans, 1)
|
|
s.Equal("/data/file2.parquet", gotOrphans[0].FilePath)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_RewritesSegmentMissingFunctionOutputs() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{
|
|
ID: 1,
|
|
CollectionID: s.collectionID,
|
|
NumOfRows: 1000,
|
|
ManifestPath: packed.MarshalManifestPath("files/insert_log/1000/2000/1", 1),
|
|
Binlogs: []*datapb.FieldBinlog{
|
|
{ChildFields: []int64{100}},
|
|
},
|
|
},
|
|
},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}},
|
|
}
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 201, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000},
|
|
}
|
|
created := []*datapb.SegmentInfo{{ID: 2, CollectionID: s.collectionID, NumOfRows: 1000}}
|
|
|
|
var checkedColumns []string
|
|
mockHasColumns := mockey.Mock(packed.ManifestHasColumns).
|
|
To(func(manifestPath string, storageConfig *indexpb.StorageConfig, columns []string) (bool, error) {
|
|
checkedColumns = columns
|
|
return false, nil
|
|
}).Build()
|
|
defer mockHasColumns.UnPatch()
|
|
|
|
var gotOrphans []packed.Fragment
|
|
mockBalance := mockey.Mock(mockey.GetMethod(task, "balanceFragmentsToSegments")).
|
|
To(func(ctx context.Context, fragments []packed.Fragment) ([]*datapb.SegmentInfo, error) {
|
|
gotOrphans = fragments
|
|
return created, nil
|
|
}).Build()
|
|
defer mockBalance.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, currentSegmentFragments, newFragments)
|
|
s.NoError(err)
|
|
s.Empty(task.GetKeptSegmentIDs())
|
|
s.Equal(created, task.GetUpdatedSegments())
|
|
s.Equal(created, result)
|
|
s.Equal([]string{"101"}, checkedColumns)
|
|
s.Require().Len(gotOrphans, 1)
|
|
s.Equal("/data/file1.parquet", gotOrphans[0].FilePath)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_KeepsSegmentWithFunctionOutputs() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{
|
|
ID: 1,
|
|
CollectionID: s.collectionID,
|
|
NumOfRows: 1000,
|
|
ManifestPath: packed.MarshalManifestPath("files/insert_log/1000/2000/1", 1),
|
|
Binlogs: []*datapb.FieldBinlog{
|
|
{ChildFields: []int64{100}},
|
|
},
|
|
},
|
|
},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}},
|
|
}
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 201, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000},
|
|
}
|
|
|
|
var checkedColumns []string
|
|
mockHasColumns := mockey.Mock(packed.ManifestHasColumns).
|
|
To(func(manifestPath string, storageConfig *indexpb.StorageConfig, columns []string) (bool, error) {
|
|
checkedColumns = columns
|
|
return true, nil
|
|
}).Build()
|
|
defer mockHasColumns.UnPatch()
|
|
|
|
var gotOrphans []packed.Fragment
|
|
mockBalance := mockey.Mock(mockey.GetMethod(task, "balanceFragmentsToSegments")).
|
|
To(func(ctx context.Context, fragments []packed.Fragment) ([]*datapb.SegmentInfo, error) {
|
|
gotOrphans = fragments
|
|
return nil, nil
|
|
}).Build()
|
|
defer mockBalance.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, currentSegmentFragments, newFragments)
|
|
s.NoError(err)
|
|
s.ElementsMatch([]int64{1}, task.GetKeptSegmentIDs())
|
|
s.Empty(task.GetUpdatedSegments())
|
|
s.Equal([]*datapb.SegmentInfo{req.GetCurrentSegments()[0]}, result)
|
|
s.Equal([]string{"101"}, checkedColumns)
|
|
s.Empty(gotOrphans)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_FunctionOutputColumnCheckError() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
CurrentSegments: []*datapb.SegmentInfo{
|
|
{ID: 1, CollectionID: s.collectionID, NumOfRows: 1000, ManifestPath: packed.MarshalManifestPath("files/insert_log/1000/2000/1", 1)},
|
|
},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
currentSegmentFragments := packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 101, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000}},
|
|
}
|
|
newFragments := []packed.Fragment{
|
|
{FragmentID: 201, FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000},
|
|
}
|
|
|
|
mockHasColumns := mockey.Mock(packed.ManifestHasColumns).
|
|
Return(false, fmt.Errorf("manifest read failed")).Build()
|
|
defer mockHasColumns.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, currentSegmentFragments, newFragments)
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
s.Contains(err.Error(), "check function output columns for segment 1")
|
|
s.Contains(err.Error(), "manifest read failed")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFunctionOutputColumnNamesUseFunctionSchema() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, OutputFieldIds: []int64{101}, OutputFieldNames: []string{"sparse"}},
|
|
},
|
|
}
|
|
|
|
columns, err := functionOutputColumnNames(schema)
|
|
s.NoError(err)
|
|
s.Equal([]string{"101"}, columns)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFunctionOutputFieldsUseFunctionSchema() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Name: "test_schema",
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{
|
|
FieldID: 101,
|
|
Name: "embedding",
|
|
DataType: schemapb.DataType_FloatVector,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "4"}},
|
|
},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{
|
|
Type: schemapb.FunctionType_TextEmbedding,
|
|
InputFieldIds: []int64{100},
|
|
OutputFieldNames: []string{"embedding"},
|
|
},
|
|
},
|
|
}
|
|
|
|
outputFields, outputSchema, err := buildOutputSchema(schema)
|
|
s.NoError(err)
|
|
s.Require().Len(outputFields, 1)
|
|
s.Equal(int64(101), outputFields[0].GetFieldID())
|
|
s.Equal("test_schema", outputSchema.GetName())
|
|
s.Equal(outputFields, outputSchema.GetFields())
|
|
|
|
bytes, err := estimateFunctionOutputBytesPerRow(schema)
|
|
s.NoError(err)
|
|
s.Equal(int64(16), bytes)
|
|
|
|
_, executionSchema, _, err := buildFunctionExecutionSchema(schema)
|
|
s.NoError(err)
|
|
s.Equal([]int64{100, 101}, []int64{
|
|
executionSchema.GetFields()[0].GetFieldID(),
|
|
executionSchema.GetFields()[1].GetFieldID(),
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFunctionOutputFieldsRejectMissingFunctionOutputs() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_TextEmbedding, OutputFieldIds: []int64{101}},
|
|
},
|
|
}
|
|
|
|
_, err := functionOutputColumnNames(schema)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function output field id 101 not found in schema")
|
|
|
|
schema.GetFunctions()[0].OutputFieldIds = nil
|
|
schema.GetFunctions()[0].OutputFieldNames = []string{"embedding"}
|
|
_, err = functionOutputColumnNames(schema)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function output field embedding not found in schema")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFunctionOutputFieldsBranches() {
|
|
outputFields, err := functionOutputFields(nil)
|
|
s.NoError(err)
|
|
s.Nil(outputFields)
|
|
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 0, Name: "zero", DataType: schemapb.DataType_VarChar, IsFunctionOutput: true},
|
|
{FieldID: 101, Name: "direct", DataType: schemapb.DataType_VarChar, IsFunctionOutput: true},
|
|
{FieldID: 102, Name: "named", DataType: schemapb.DataType_SparseFloatVector},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{
|
|
OutputFieldIds: []int64{0, 102},
|
|
OutputFieldNames: []string{"", "named"},
|
|
},
|
|
},
|
|
}
|
|
|
|
outputFields, err = functionOutputFields(schema)
|
|
s.NoError(err)
|
|
s.Equal([]int64{101, 102}, []int64{
|
|
outputFields[0].GetFieldID(),
|
|
outputFields[1].GetFieldID(),
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBuildFunctionExecutionSchemaBranches() {
|
|
_, _, _, err := buildFunctionExecutionSchema(nil)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "collection schema is nil")
|
|
|
|
_, _, err = buildOutputSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function output field id 101 not found")
|
|
|
|
_, _, _, err = buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{999}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function input field id 999 not found in schema")
|
|
|
|
_, _, _, err = buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function output field id 101 not found")
|
|
|
|
_, _, _, err = buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: common.VirtualPKFieldName, DataType: schemapb.DataType_Int64},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "no source input columns")
|
|
|
|
_, _, _, err = buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{100}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "no source input columns")
|
|
|
|
inputSchema, executionSchema, requiredFields, err := buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
{FieldID: 100, Name: "text_dup", DataType: schemapb.DataType_VarChar},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.NoError(err)
|
|
s.Require().NotNil(inputSchema)
|
|
s.Require().NotNil(executionSchema)
|
|
s.True(requiredFields.Contain(int64(100)))
|
|
s.Len(executionSchema.GetFields(), 2)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSegmentHasFunctionOutputColumnsBranches() {
|
|
task := NewRefreshExternalCollectionTask(context.Background(), &datapb.RefreshExternalCollectionTaskRequest{})
|
|
|
|
hasColumns, err := task.segmentHasFunctionOutputColumns(&datapb.SegmentInfo{}, nil)
|
|
s.NoError(err)
|
|
s.True(hasColumns)
|
|
|
|
hasColumns, err = task.segmentHasFunctionOutputColumns(&datapb.SegmentInfo{}, []string{"101"})
|
|
s.NoError(err)
|
|
s.False(hasColumns)
|
|
|
|
mock := mockey.Mock(packed.ManifestHasColumns).To(
|
|
func(string, *indexpb.StorageConfig, []string) (bool, error) {
|
|
s.FailNow("ManifestHasColumns should not be called when ChildFields already prove the output field exists")
|
|
return false, nil
|
|
}).Build()
|
|
defer mock.UnPatch()
|
|
hasColumns, err = task.segmentHasFunctionOutputColumns(&datapb.SegmentInfo{
|
|
ManifestPath: "manifest",
|
|
Binlogs: []*datapb.FieldBinlog{
|
|
{ChildFields: []int64{100, 101}},
|
|
},
|
|
}, []string{"101"})
|
|
s.NoError(err)
|
|
s.True(hasColumns)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegmentsFunctionOutputColumnError() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, OutputFieldIds: []int64{101}},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
result, err := task.organizeSegments(ctx, nil, nil)
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
s.Contains(err.Error(), "resolve function output columns")
|
|
|
|
_, err = estimateFunctionOutputBytesPerRow(req.GetSchema())
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "function output field id 101 not found")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegmentsBalanceError() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
mockBalance := mockey.Mock(mockey.GetMethod(task, "balanceFragmentsToSegments")).
|
|
Return(nil, fmt.Errorf("balance failed")).Build()
|
|
defer mockBalance.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, nil, []packed.Fragment{{FragmentID: 1, FilePath: "/data/file.parquet", RowCount: 100}})
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegmentsContextCanceledDuringLoops() {
|
|
testCases := []struct {
|
|
name string
|
|
cancelAt int
|
|
currentSegments []*datapb.SegmentInfo
|
|
currentSegmentFragments packed.SegmentFragments
|
|
newFragments []packed.Fragment
|
|
}{
|
|
{
|
|
name: "current segment loop",
|
|
cancelAt: 2,
|
|
currentSegments: []*datapb.SegmentInfo{{ID: 1, CollectionID: s.collectionID, NumOfRows: 100}},
|
|
currentSegmentFragments: packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 1, FilePath: "/data/file1.parquet", RowCount: 100}},
|
|
},
|
|
newFragments: []packed.Fragment{{FragmentID: 1, FilePath: "/data/file1.parquet", RowCount: 100}},
|
|
},
|
|
{
|
|
name: "kept fragment loop",
|
|
cancelAt: 3,
|
|
currentSegments: []*datapb.SegmentInfo{{ID: 1, CollectionID: s.collectionID, NumOfRows: 100}},
|
|
currentSegmentFragments: packed.SegmentFragments{
|
|
1: []packed.Fragment{{FragmentID: 1, FilePath: "/data/file1.parquet", RowCount: 100}},
|
|
},
|
|
newFragments: []packed.Fragment{{FragmentID: 1, FilePath: "/data/file1.parquet", RowCount: 100}},
|
|
},
|
|
{
|
|
name: "new fragment loop",
|
|
cancelAt: 2,
|
|
newFragments: []packed.Fragment{{FragmentID: 1, FilePath: "/data/file1.parquet", RowCount: 100}},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.T().Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
CurrentSegments: tc.currentSegments,
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
var calls int
|
|
mockEnsure := mockey.Mock(ensureContext).To(func(ctx context.Context) error {
|
|
calls++
|
|
if calls == tc.cancelAt {
|
|
return context.Canceled
|
|
}
|
|
return nil
|
|
}).Build()
|
|
defer mockEnsure.UnPatch()
|
|
|
|
result, err := task.organizeSegments(ctx, tc.currentSegmentFragments, tc.newFragments)
|
|
s.ErrorIs(err, context.Canceled)
|
|
s.Nil(result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOrganizeSegments_NewFragmentsAdded() {
|
|
s.T().Skip("Skip test that requires CGO FFI calls for manifest creation")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_SmallFile() {
|
|
// File smaller than limit - should return single fragment
|
|
fragments := packed.SplitFileToFragments("/data/small.parquet", 500000, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(0))
|
|
|
|
s.Len(fragments, 1)
|
|
s.Equal(int64(0), fragments[0].FragmentID)
|
|
s.Equal("/data/small.parquet", fragments[0].FilePath)
|
|
s.Equal(int64(0), fragments[0].StartRow)
|
|
s.Equal(int64(500000), fragments[0].EndRow)
|
|
s.Equal(int64(500000), fragments[0].RowCount)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_ExactLimit() {
|
|
// File exactly at limit - should return single fragment
|
|
fragments := packed.SplitFileToFragments("/data/exact.parquet", packed.DefaultFragmentRowLimit, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(0))
|
|
|
|
s.Len(fragments, 1)
|
|
s.Equal(int64(0), fragments[0].FragmentID)
|
|
s.Equal(int64(0), fragments[0].StartRow)
|
|
s.Equal(int64(packed.DefaultFragmentRowLimit), fragments[0].EndRow)
|
|
s.Equal(int64(packed.DefaultFragmentRowLimit), fragments[0].RowCount)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_LargeFile() {
|
|
// File with 2.5 million rows - should split into 3 fragments
|
|
totalRows := int64(2500000)
|
|
fragments := packed.SplitFileToFragments("/data/large.parquet", totalRows, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(0))
|
|
|
|
s.Len(fragments, 3)
|
|
|
|
// First fragment: 0-1000000
|
|
s.Equal(int64(0), fragments[0].FragmentID)
|
|
s.Equal("/data/large.parquet", fragments[0].FilePath)
|
|
s.Equal(int64(0), fragments[0].StartRow)
|
|
s.Equal(int64(1000000), fragments[0].EndRow)
|
|
s.Equal(int64(1000000), fragments[0].RowCount)
|
|
|
|
// Second fragment: 1000000-2000000
|
|
s.Equal(int64(1), fragments[1].FragmentID)
|
|
s.Equal("/data/large.parquet", fragments[1].FilePath)
|
|
s.Equal(int64(1000000), fragments[1].StartRow)
|
|
s.Equal(int64(2000000), fragments[1].EndRow)
|
|
s.Equal(int64(1000000), fragments[1].RowCount)
|
|
|
|
// Third fragment: 2000000-2500000
|
|
s.Equal(int64(2), fragments[2].FragmentID)
|
|
s.Equal("/data/large.parquet", fragments[2].FilePath)
|
|
s.Equal(int64(2000000), fragments[2].StartRow)
|
|
s.Equal(int64(2500000), fragments[2].EndRow)
|
|
s.Equal(int64(500000), fragments[2].RowCount)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_BaseFragmentID() {
|
|
// Test with non-zero base fragment ID
|
|
fragments := packed.SplitFileToFragments("/data/test.parquet", 2500000, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(100))
|
|
|
|
s.Len(fragments, 3)
|
|
s.Equal(int64(100), fragments[0].FragmentID)
|
|
s.Equal(int64(101), fragments[1].FragmentID)
|
|
s.Equal(int64(102), fragments[2].FragmentID)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_ZeroRows() {
|
|
// Empty file - should return single fragment with zero rows
|
|
fragments := packed.SplitFileToFragments("/data/empty.parquet", 0, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(0))
|
|
|
|
s.Len(fragments, 1)
|
|
s.Equal(int64(0), fragments[0].RowCount)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSplitFileToFragments_TenMillionRows() {
|
|
// 10 million rows - should split into 10 fragments
|
|
totalRows := int64(10000000)
|
|
fragments := packed.SplitFileToFragments("/data/huge.parquet", totalRows, packed.DefaultFragmentRowLimit, packed.NewFragmentIDGenerator(0))
|
|
|
|
s.Len(fragments, 10)
|
|
|
|
// Verify total rows across all fragments
|
|
var totalFragmentRows int64
|
|
for _, f := range fragments {
|
|
totalFragmentRows += f.RowCount
|
|
}
|
|
s.Equal(totalRows, totalFragmentRows)
|
|
|
|
// Verify fragment IDs are sequential
|
|
for i, f := range fragments {
|
|
s.Equal(int64(i), f.FragmentID)
|
|
}
|
|
|
|
// Verify ranges are continuous
|
|
for i := 1; i < len(fragments); i++ {
|
|
s.Equal(fragments[i-1].EndRow, fragments[i].StartRow)
|
|
}
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestCreateManifestForSegment() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: 2000,
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"col1"}
|
|
|
|
var gotBasePath string
|
|
var gotStorageConfig *indexpb.StorageConfig
|
|
var gotExtfs packed.ExternalSpecContext
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(
|
|
ctx context.Context,
|
|
basePath string,
|
|
format string,
|
|
columns []string,
|
|
fragments []packed.Fragment,
|
|
storageConfig *indexpb.StorageConfig,
|
|
extfs packed.ExternalSpecContext,
|
|
) (string, error) {
|
|
gotBasePath = basePath
|
|
gotStorageConfig = storageConfig
|
|
gotExtfs = extfs
|
|
return "manifest-path", nil
|
|
}).Build()
|
|
defer mockCreate.UnPatch()
|
|
|
|
manifestPath, err := task.createManifestForSegment(ctx, 3000, []packed.Fragment{{FragmentID: 1}})
|
|
s.NoError(err)
|
|
s.Equal("manifest-path", manifestPath)
|
|
s.Equal("files/insert_log/1000/2000/3000", gotBasePath)
|
|
s.Same(req.GetStorageConfig(), gotStorageConfig)
|
|
s.Equal(s.collectionID, gotExtfs.CollectionID)
|
|
s.Empty(gotExtfs.Source)
|
|
s.Empty(gotExtfs.Spec)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestCreateManifestWithFunctionsUsesInsertLogBasePath() {
|
|
paramtable.Init()
|
|
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: 2000,
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{
|
|
Name: "bm25",
|
|
Type: schemapb.FunctionType_BM25,
|
|
InputFieldIds: []int64{100},
|
|
OutputFieldIds: []int64{101},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
var gotBasePath string
|
|
var gotStorageConfig *indexpb.StorageConfig
|
|
mockExec := mockey.Mock(ExecuteFunctionsForSegment).
|
|
To(func(
|
|
ctx context.Context,
|
|
schema *schemapb.CollectionSchema,
|
|
fragments []packed.Fragment,
|
|
format string,
|
|
storageConfig *indexpb.StorageConfig,
|
|
collectionID int64,
|
|
segmentID int64,
|
|
basePath string,
|
|
clusterID string,
|
|
) (string, error) {
|
|
gotBasePath = basePath
|
|
gotStorageConfig = storageConfig
|
|
return "manifest-path", nil
|
|
}).Build()
|
|
defer mockExec.UnPatch()
|
|
|
|
manifestPath, err := task.createManifestWithFunctions(ctx, 3000, []packed.Fragment{{FragmentID: 1}})
|
|
s.NoError(err)
|
|
s.Equal("manifest-path", manifestPath)
|
|
s.Equal("files/insert_log/1000/2000/3000", gotBasePath)
|
|
s.Same(req.GetStorageConfig(), gotStorageConfig)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteFunctionsForSegmentUsesProvidedBasePath() {
|
|
ctx := context.Background()
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
schema := &schemapb.CollectionSchema{
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
},
|
|
}
|
|
fragments := []packed.Fragment{{FragmentID: 1, FilePath: "s3://bucket/path/file.parquet"}}
|
|
|
|
var gotBasePath string
|
|
var gotColumns []string
|
|
var gotFragments []packed.Fragment
|
|
var gotStorageConfig *indexpb.StorageConfig
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(
|
|
ctx context.Context,
|
|
basePath string,
|
|
format string,
|
|
columns []string,
|
|
fragments []packed.Fragment,
|
|
storageConfig *indexpb.StorageConfig,
|
|
extfs packed.ExternalSpecContext,
|
|
) (string, error) {
|
|
gotBasePath = basePath
|
|
gotColumns = columns
|
|
gotFragments = fragments
|
|
gotStorageConfig = storageConfig
|
|
return "", fmt.Errorf("mock create manifest")
|
|
}).Build()
|
|
defer mockCreate.UnPatch()
|
|
|
|
manifestPath, err := ExecuteFunctionsForSegment(
|
|
ctx,
|
|
schema,
|
|
fragments,
|
|
"parquet",
|
|
storageConfig,
|
|
s.collectionID,
|
|
3000,
|
|
"files/insert_log/1000/2000/3000",
|
|
"cluster",
|
|
)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "create input manifest")
|
|
s.Empty(manifestPath)
|
|
s.Equal("files/insert_log/1000/2000/3000", gotBasePath)
|
|
s.Equal([]string{"text_col"}, gotColumns)
|
|
s.Equal(fragments, gotFragments)
|
|
s.Same(storageConfig, gotStorageConfig)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteFunctionsForSegmentRequiresFunctionOutputs() {
|
|
ctx := context.Background()
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
},
|
|
}
|
|
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(
|
|
ctx context.Context,
|
|
basePath string,
|
|
format string,
|
|
columns []string,
|
|
fragments []packed.Fragment,
|
|
storageConfig *indexpb.StorageConfig,
|
|
extfs packed.ExternalSpecContext,
|
|
) (string, error) {
|
|
return packed.MarshalManifestPath(basePath, 42), nil
|
|
}).Build()
|
|
defer mockCreate.UnPatch()
|
|
|
|
manifestPath, err := ExecuteFunctionsForSegment(
|
|
ctx,
|
|
schema,
|
|
[]packed.Fragment{{FragmentID: 1}},
|
|
"parquet",
|
|
&indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
s.collectionID,
|
|
3000,
|
|
"files/insert_log/1000/2000/3000",
|
|
"cluster",
|
|
)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "no function output fields")
|
|
s.Empty(manifestPath)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFunctionExecutorSchemaHelpers() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Name: "test_schema",
|
|
ExternalSource: "s3://bucket/path",
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
{FieldID: 102, Name: "vec", DataType: schemapb.DataType_FloatVector, ExternalField: "vec_col", TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "2"}}},
|
|
{FieldID: 103, Name: common.VirtualPKFieldName, DataType: schemapb.DataType_Int64},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
}
|
|
|
|
inputSchema, executionSchema, requiredFields, err := buildFunctionExecutionSchema(schema)
|
|
s.NoError(err)
|
|
s.True(requiredFields.Contain(int64(100)))
|
|
s.Require().Len(inputSchema.GetFields(), 1)
|
|
s.Equal("text", inputSchema.GetFields()[0].GetName())
|
|
s.Require().Len(executionSchema.GetFields(), 2)
|
|
s.Equal([]int64{100, 101}, []int64{
|
|
executionSchema.GetFields()[0].GetFieldID(),
|
|
executionSchema.GetFields()[1].GetFieldID(),
|
|
})
|
|
|
|
outputFields, outputSchema, err := buildOutputSchema(schema)
|
|
s.NoError(err)
|
|
s.Require().Len(outputFields, 1)
|
|
s.Equal(int64(101), outputFields[0].GetFieldID())
|
|
s.Equal("test_schema", outputSchema.GetName())
|
|
s.Equal(outputFields, outputSchema.GetFields())
|
|
|
|
_, _, _, err = buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
ExternalSource: "s3://bucket/path",
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "has no external_field")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBM25Accumulators() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, OutputFieldIds: []int64{101, 102}},
|
|
{Type: schemapb.FunctionType_TextEmbedding, OutputFieldIds: []int64{103}},
|
|
},
|
|
}
|
|
|
|
acc := newBM25Accumulators(schema)
|
|
s.Require().Len(acc, 2)
|
|
s.NotNil(acc[101])
|
|
s.NotNil(acc[102])
|
|
s.Nil(acc[103])
|
|
|
|
err := accumulateBM25Stats(&storage.InsertData{Data: map[int64]storage.FieldData{}}, map[int64]*storage.BM25Stats{
|
|
101: storage.NewBM25Stats(),
|
|
})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "missing from batch")
|
|
|
|
err = accumulateBM25Stats(&storage.InsertData{Data: map[int64]storage.FieldData{
|
|
101: &storage.StringFieldData{Data: []string{"wrong"}},
|
|
}}, map[int64]*storage.BM25Stats{101: storage.NewBM25Stats()})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "wrong type")
|
|
|
|
stats := storage.NewBM25Stats()
|
|
err = accumulateBM25Stats(&storage.InsertData{Data: map[int64]storage.FieldData{
|
|
101: &storage.SparseFloatVectorFieldData{
|
|
SparseFloatArray: schemapb.SparseFloatArray{
|
|
Contents: [][]byte{
|
|
typeutil.CreateSparseFloatRow([]uint32{1, 2}, []float32{3, 4}),
|
|
typeutil.CreateSparseFloatRow([]uint32{2}, []float32{5}),
|
|
},
|
|
},
|
|
},
|
|
}}, map[int64]*storage.BM25Stats{101: stats})
|
|
s.NoError(err)
|
|
s.Equal(int64(2), stats.NumRow())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFinalizeBM25Stats() {
|
|
ctx := context.Background()
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
manifestPath := packed.MarshalManifestPath("files/insert_log/1000/2000/3000", 42)
|
|
|
|
gotManifest, err := finalizeBM25Stats(ctx, nil, storageConfig, manifestPath)
|
|
s.NoError(err)
|
|
s.Equal(manifestPath, gotManifest)
|
|
|
|
gotManifest, err = finalizeBM25Stats(ctx, map[int64]*storage.BM25Stats{
|
|
101: storage.NewBM25Stats(),
|
|
}, storageConfig, "bad manifest")
|
|
s.Error(err)
|
|
s.Empty(gotManifest)
|
|
|
|
stats := storage.NewBM25Stats()
|
|
stats.Append(map[uint32]float32{1: 2})
|
|
var gotFilePath string
|
|
var gotEntries []packed.StatEntry
|
|
mockWrite := mockey.Mock(packed.WriteFile).
|
|
To(func(storageConfig *indexpb.StorageConfig, filePath string, data []byte) error {
|
|
gotFilePath = filePath
|
|
s.NotEmpty(data)
|
|
return nil
|
|
}).Build()
|
|
defer mockWrite.UnPatch()
|
|
mockCommit := mockey.Mock(packed.CommitManifestUpdates).
|
|
To(func(basePath string, version int64, storageConfig *indexpb.StorageConfig, updates *packed.ManifestUpdates) (string, error) {
|
|
s.Equal("files/insert_log/1000/2000/3000", basePath)
|
|
s.Equal(int64(42), version)
|
|
gotEntries = updates.Stats
|
|
return "updated-manifest", nil
|
|
}).Build()
|
|
defer mockCommit.UnPatch()
|
|
|
|
gotManifest, err = finalizeBM25Stats(ctx, map[int64]*storage.BM25Stats{101: stats}, storageConfig, manifestPath)
|
|
s.NoError(err)
|
|
s.Equal("updated-manifest", gotManifest)
|
|
s.Equal("files/insert_log/1000/2000/3000/_stats/bm25.101/0", gotFilePath)
|
|
s.Require().Len(gotEntries, 1)
|
|
s.Equal("bm25.101", gotEntries[0].Key)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFinalizeBM25StatsErrors() {
|
|
ctx := context.Background()
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
manifestPath := packed.MarshalManifestPath("files/insert_log/1000/2000/3000", 42)
|
|
stats := storage.NewBM25Stats()
|
|
stats.Append(map[uint32]float32{1: 2})
|
|
|
|
mockSerialize := mockey.Mock(mockey.GetMethod(stats, "Serialize")).Return(nil, fmt.Errorf("serialize failed")).Build()
|
|
gotManifest, err := finalizeBM25Stats(ctx, map[int64]*storage.BM25Stats{101: stats}, storageConfig, manifestPath)
|
|
s.Error(err)
|
|
s.Empty(gotManifest)
|
|
mockSerialize.UnPatch()
|
|
|
|
mockWrite := mockey.Mock(packed.WriteFile).Return(fmt.Errorf("write failed")).Build()
|
|
gotManifest, err = finalizeBM25Stats(ctx, map[int64]*storage.BM25Stats{101: stats}, storageConfig, manifestPath)
|
|
s.Error(err)
|
|
s.Empty(gotManifest)
|
|
mockWrite.UnPatch()
|
|
|
|
mockWrite = mockey.Mock(packed.WriteFile).Return(nil).Build()
|
|
defer mockWrite.UnPatch()
|
|
mockCommit := mockey.Mock(packed.CommitManifestUpdates).Return("", fmt.Errorf("commit failed")).Build()
|
|
defer mockCommit.UnPatch()
|
|
gotManifest, err = finalizeBM25Stats(ctx, map[int64]*storage.BM25Stats{101: stats}, storageConfig, manifestPath)
|
|
s.Error(err)
|
|
s.Empty(gotManifest)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestWriteOutputBatch() {
|
|
outputSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector},
|
|
},
|
|
}
|
|
outputArrow, err := storage.ConvertToArrowSchema(outputSchema, true)
|
|
s.NoError(err)
|
|
|
|
writer := &packed.FFIPackedWriter{}
|
|
var wroteRows int64
|
|
mockWrite := mockey.Mock(mockey.GetMethod(writer, "WriteRecordBatch")).
|
|
To(func(recordBatch arrow.Record) error {
|
|
wroteRows = recordBatch.NumRows()
|
|
return nil
|
|
}).Build()
|
|
defer mockWrite.UnPatch()
|
|
|
|
err = writeOutputBatch(&storage.InsertData{Data: map[int64]storage.FieldData{
|
|
101: &storage.SparseFloatVectorFieldData{
|
|
SparseFloatArray: schemapb.SparseFloatArray{
|
|
Contents: [][]byte{typeutil.CreateSparseFloatRow([]uint32{1}, []float32{2})},
|
|
},
|
|
},
|
|
}}, outputSchema, outputArrow, writer)
|
|
s.NoError(err)
|
|
s.Equal(int64(1), wroteRows)
|
|
|
|
err = writeOutputBatch(&storage.InsertData{Data: map[int64]storage.FieldData{}}, outputSchema, outputArrow, writer)
|
|
s.Error(err)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestOpenInputReader() {
|
|
schema := &schemapb.CollectionSchema{
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
{FieldID: 102, Name: "vec", DataType: schemapb.DataType_FloatVector, ExternalField: "vec_col", TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "2"}}},
|
|
},
|
|
}
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
mockEncryption := mockey.Mock(hookutil.IsClusterEncryptionEnabled).Return(false).Build()
|
|
defer mockEncryption.UnPatch()
|
|
|
|
var gotColumns []string
|
|
var gotExt packed.ExternalReaderContext
|
|
var gotSchema *arrow.Schema
|
|
mockReader := mockey.Mock(packed.NewFFIPackedReader).
|
|
To(func(
|
|
manifestPath string,
|
|
schema *arrow.Schema,
|
|
neededColumns []string,
|
|
bufferSize int64,
|
|
storageConfig *indexpb.StorageConfig,
|
|
storagePluginContext *indexcgopb.StoragePluginContext,
|
|
ext packed.ExternalReaderContext,
|
|
) (*packed.FFIPackedReader, error) {
|
|
gotColumns = neededColumns
|
|
gotSchema = schema
|
|
gotExt = ext
|
|
return &packed.FFIPackedReader{}, nil
|
|
}).Build()
|
|
|
|
inputSchema, _, _, err := buildFunctionExecutionSchema(&schemapb.CollectionSchema{
|
|
ExternalSource: schema.GetExternalSource(),
|
|
ExternalSpec: schema.GetExternalSpec(),
|
|
Fields: schema.GetFields(),
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
})
|
|
s.NoError(err)
|
|
reader, err := openInputReader(context.Background(), schema, "manifest", inputSchema, storageConfig, s.collectionID)
|
|
s.NoError(err)
|
|
s.NotNil(reader)
|
|
s.Equal([]string{"text_col"}, gotColumns)
|
|
s.Require().NotNil(gotSchema)
|
|
s.Equal(1, gotSchema.NumFields())
|
|
s.Equal("text_col", gotSchema.Field(0).Name)
|
|
s.Equal(arrow.BinaryTypes.String, gotSchema.Field(0).Type)
|
|
s.Equal(s.collectionID, gotExt.CollectionID)
|
|
s.Equal("s3://bucket/path", gotExt.Source)
|
|
s.Equal(`{"format":"parquet"}`, gotExt.Spec)
|
|
mockReader.UnPatch()
|
|
|
|
mockReader = mockey.Mock(packed.NewFFIPackedReader).Return(nil, fmt.Errorf("open failed")).Build()
|
|
defer mockReader.UnPatch()
|
|
reader, err = openInputReader(context.Background(), schema, "manifest", inputSchema, storageConfig, s.collectionID)
|
|
s.Error(err)
|
|
s.Nil(reader)
|
|
s.Contains(err.Error(), "open input manifest")
|
|
|
|
reader, err = openInputReader(context.Background(), &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "bad", DataType: schemapb.DataType_None}},
|
|
}, "manifest", &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "bad", DataType: schemapb.DataType_None}},
|
|
}, storageConfig, s.collectionID)
|
|
s.Error(err)
|
|
s.Nil(reader)
|
|
s.Contains(err.Error(), "open input manifest")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestStreamBatchesSuccess() {
|
|
schema := &schemapb.CollectionSchema{DbName: "db"}
|
|
writer := &packed.FFIPackedWriter{}
|
|
executionSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
}
|
|
reader := &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(100, "text_col", []string{"hello"})},
|
|
}
|
|
mockRun := mockey.Mock(embedding.RunAll).
|
|
To(func(ctx context.Context, schema *schemapb.CollectionSchema, data *storage.InsertData, opts embedding.RunOptions) error {
|
|
s.Equal("cluster", opts.ClusterID)
|
|
s.Equal("db", opts.DBName)
|
|
s.Equal(1, data.GetRowNum())
|
|
return nil
|
|
}).Build()
|
|
defer mockRun.UnPatch()
|
|
mockAcc := mockey.Mock(accumulateBM25Stats).Return(nil).Build()
|
|
defer mockAcc.UnPatch()
|
|
mockWrite := mockey.Mock(writeOutputBatch).Return(nil).Build()
|
|
defer mockWrite.UnPatch()
|
|
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
typeutil.NewSet[int64](100), reader, writer, nil, "cluster")
|
|
s.NoError(err)
|
|
s.Equal(int64(1), rows)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestStreamBatchesErrors() {
|
|
schema := &schemapb.CollectionSchema{}
|
|
executionSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
}
|
|
writer := &packed.FFIPackedWriter{}
|
|
requiredFields := typeutil.NewSet[int64](100)
|
|
|
|
reader := &fakeRecordReader{errs: []error{fmt.Errorf("read failed")}}
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.Error(err)
|
|
s.Equal(int64(0), rows)
|
|
|
|
reader = &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(999, "wrong_col", []string{"hello"})},
|
|
}
|
|
rows, err = streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.Error(err)
|
|
s.Equal(int64(0), rows)
|
|
|
|
reader = &fakeRecordReader{}
|
|
rows, err = streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.NoError(err)
|
|
s.Equal(int64(0), rows)
|
|
|
|
reader = &fakeRecordReader{records: []storage.Record{nil}}
|
|
rows, err = streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.NoError(err)
|
|
s.Equal(int64(0), rows)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestStreamBatchesProcessingErrors() {
|
|
schema := &schemapb.CollectionSchema{DbName: "db"}
|
|
writer := &packed.FFIPackedWriter{}
|
|
executionSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
}
|
|
requiredFields := typeutil.NewSet[int64](100)
|
|
|
|
s.T().Run("empty batch", func(t *testing.T) {
|
|
reader := &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(100, "text_col", nil)},
|
|
}
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.NoError(err)
|
|
s.Equal(int64(0), rows)
|
|
})
|
|
|
|
s.T().Run("function error", func(t *testing.T) {
|
|
reader := &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(100, "text_col", []string{"hello"})},
|
|
}
|
|
mockRun := mockey.Mock(embedding.RunAll).Return(fmt.Errorf("run failed")).Build()
|
|
defer mockRun.UnPatch()
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.Error(err)
|
|
s.Equal(int64(0), rows)
|
|
})
|
|
|
|
s.T().Run("bm25 stats error", func(t *testing.T) {
|
|
reader := &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(100, "text_col", []string{"hello"})},
|
|
}
|
|
mockRun := mockey.Mock(embedding.RunAll).Return(nil).Build()
|
|
defer mockRun.UnPatch()
|
|
mockAcc := mockey.Mock(accumulateBM25Stats).Return(fmt.Errorf("stats failed")).Build()
|
|
defer mockAcc.UnPatch()
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.Error(err)
|
|
s.Equal(int64(0), rows)
|
|
})
|
|
|
|
s.T().Run("write error", func(t *testing.T) {
|
|
reader := &fakeRecordReader{
|
|
records: []storage.Record{s.makeStringStorageRecord(100, "text_col", []string{"hello"})},
|
|
}
|
|
mockRun := mockey.Mock(embedding.RunAll).Return(nil).Build()
|
|
defer mockRun.UnPatch()
|
|
mockAcc := mockey.Mock(accumulateBM25Stats).Return(nil).Build()
|
|
defer mockAcc.UnPatch()
|
|
mockWrite := mockey.Mock(writeOutputBatch).Return(fmt.Errorf("write failed")).Build()
|
|
defer mockWrite.UnPatch()
|
|
rows, err := streamBatches(context.Background(), schema, executionSchema, nil, nil,
|
|
requiredFields, reader, writer, nil, "cluster")
|
|
s.Error(err)
|
|
s.Equal(int64(0), rows)
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteFunctionsForSegmentSuccess() {
|
|
ctx := context.Background()
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
{FieldID: 102, Name: "vec", DataType: schemapb.DataType_FloatVector, ExternalField: "vec_col", TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "2"}}},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
}
|
|
reader := &fakeRecordReader{}
|
|
writer := &packed.FFIPackedWriter{}
|
|
basePath := "files/insert_log/1000/2000/3000"
|
|
|
|
var sourceColumns []string
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(
|
|
ctx context.Context,
|
|
basePath string,
|
|
format string,
|
|
columns []string,
|
|
fragments []packed.Fragment,
|
|
storageConfig *indexpb.StorageConfig,
|
|
extfs packed.ExternalSpecContext,
|
|
) (string, error) {
|
|
sourceColumns = columns
|
|
return packed.MarshalManifestPath(basePath, 42), nil
|
|
}).Build()
|
|
defer mockCreate.UnPatch()
|
|
var gotInputSchema *schemapb.CollectionSchema
|
|
mockOpen := mockey.Mock(openInputReader).
|
|
To(func(
|
|
ctx context.Context,
|
|
schema *schemapb.CollectionSchema,
|
|
manifestPath string,
|
|
inputSchema *schemapb.CollectionSchema,
|
|
storageConfig *indexpb.StorageConfig,
|
|
collectionID int64,
|
|
) (storage.RecordReader, error) {
|
|
gotInputSchema = inputSchema
|
|
return reader, nil
|
|
}).Build()
|
|
defer mockOpen.UnPatch()
|
|
mockNewWriter := mockey.Mock(packed.NewFFIPackedWriter).
|
|
To(func(
|
|
basePath string,
|
|
schema *arrow.Schema,
|
|
columnGroups []storagecommon.ColumnGroup,
|
|
storageConfig *indexpb.StorageConfig,
|
|
storagePluginContext *indexcgopb.StoragePluginContext,
|
|
extraProperties ...map[string]string,
|
|
) (*packed.FFIPackedWriter, error) {
|
|
s.Require().NotNil(schema)
|
|
s.Equal("101", schema.Field(0).Name)
|
|
s.Require().Len(columnGroups, 1)
|
|
s.Equal([]int{0}, columnGroups[0].Columns)
|
|
return writer, nil
|
|
}).Build()
|
|
defer mockNewWriter.UnPatch()
|
|
mockStream := mockey.Mock(streamBatches).Return(int64(7), nil).Build()
|
|
defer mockStream.UnPatch()
|
|
output := new(packed.ColumnGroups)
|
|
mockClose := mockey.Mock(mockey.GetMethod(writer, "Close")).Return(output, nil).Build()
|
|
defer mockClose.UnPatch()
|
|
mockAppend := mockey.Mock(appendBM25Stats).Return(nil).Build()
|
|
defer mockAppend.UnPatch()
|
|
mockCommit := mockey.Mock(packed.CommitManifestUpdates).
|
|
To(func(basePath string, version int64, storageConfig *indexpb.StorageConfig, updates *packed.ManifestUpdates) (string, error) {
|
|
s.Equal(int64(42), version)
|
|
s.Same(output, updates.NewFiles)
|
|
return "final-manifest", nil
|
|
}).Build()
|
|
defer mockCommit.UnPatch()
|
|
|
|
manifestPath, err := ExecuteFunctionsForSegment(
|
|
ctx,
|
|
schema,
|
|
[]packed.Fragment{{FragmentID: 1}},
|
|
"parquet",
|
|
storageConfig,
|
|
s.collectionID,
|
|
3000,
|
|
basePath,
|
|
"cluster",
|
|
)
|
|
s.NoError(err)
|
|
s.Equal("final-manifest", manifestPath)
|
|
s.Equal([]string{"text_col", "vec_col"}, sourceColumns)
|
|
s.Require().NotNil(gotInputSchema)
|
|
s.Require().Len(gotInputSchema.GetFields(), 1)
|
|
s.Equal(int64(100), gotInputSchema.GetFields()[0].GetFieldID())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteFunctionsForSegmentErrorPaths() {
|
|
ctx := context.Background()
|
|
storageConfig := &indexpb.StorageConfig{RootPath: "files", StorageType: "local"}
|
|
basePath := "files/insert_log/1000/2000/3000"
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
}
|
|
|
|
s.T().Run("parse input manifest", func(t *testing.T) {
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return("bad manifest", nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("output schema conversion", func(t *testing.T) {
|
|
badSchema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 101, Name: "bad", DataType: schemapb.DataType_None, IsFunctionOutput: true},
|
|
},
|
|
}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, badSchema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("execution schema", func(t *testing.T) {
|
|
badSchema := &schemapb.CollectionSchema{
|
|
ExternalSource: "s3://bucket/path",
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, badSchema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
s.Contains(err.Error(), "has no external_field")
|
|
})
|
|
|
|
s.T().Run("open reader", func(t *testing.T) {
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
mockOpen := mockey.Mock(openInputReader).Return(nil, fmt.Errorf("open failed")).Build()
|
|
defer mockOpen.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("new writer", func(t *testing.T) {
|
|
reader := &fakeRecordReader{}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
mockOpen := mockey.Mock(openInputReader).Return(reader, nil).Build()
|
|
defer mockOpen.UnPatch()
|
|
mockWriter := mockey.Mock(packed.NewFFIPackedWriter).Return(nil, fmt.Errorf("writer failed")).Build()
|
|
defer mockWriter.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("stream", func(t *testing.T) {
|
|
reader := &fakeRecordReader{}
|
|
writer := &packed.FFIPackedWriter{}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
mockOpen := mockey.Mock(openInputReader).Return(reader, nil).Build()
|
|
defer mockOpen.UnPatch()
|
|
mockWriter := mockey.Mock(packed.NewFFIPackedWriter).Return(writer, nil).Build()
|
|
defer mockWriter.UnPatch()
|
|
mockStream := mockey.Mock(streamBatches).Return(int64(0), fmt.Errorf("stream failed")).Build()
|
|
defer mockStream.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("close writer", func(t *testing.T) {
|
|
reader := &fakeRecordReader{}
|
|
writer := &packed.FFIPackedWriter{}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
mockOpen := mockey.Mock(openInputReader).Return(reader, nil).Build()
|
|
defer mockOpen.UnPatch()
|
|
mockWriter := mockey.Mock(packed.NewFFIPackedWriter).Return(writer, nil).Build()
|
|
defer mockWriter.UnPatch()
|
|
mockStream := mockey.Mock(streamBatches).Return(int64(1), nil).Build()
|
|
defer mockStream.UnPatch()
|
|
mockClose := mockey.Mock(mockey.GetMethod(writer, "Close")).Return(nil, fmt.Errorf("close failed")).Build()
|
|
defer mockClose.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
|
|
s.T().Run("finalize stats", func(t *testing.T) {
|
|
reader := &fakeRecordReader{}
|
|
writer := &packed.FFIPackedWriter{}
|
|
mockCreate := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).Return(packed.MarshalManifestPath(basePath, 42), nil).Build()
|
|
defer mockCreate.UnPatch()
|
|
mockOpen := mockey.Mock(openInputReader).Return(reader, nil).Build()
|
|
defer mockOpen.UnPatch()
|
|
mockWriter := mockey.Mock(packed.NewFFIPackedWriter).Return(writer, nil).Build()
|
|
defer mockWriter.UnPatch()
|
|
mockStream := mockey.Mock(streamBatches).Return(int64(1), nil).Build()
|
|
defer mockStream.UnPatch()
|
|
mockClose := mockey.Mock(mockey.GetMethod(writer, "Close")).Return(new(packed.ColumnGroups), nil).Build()
|
|
defer mockClose.UnPatch()
|
|
mockAppend := mockey.Mock(appendBM25Stats).Return(fmt.Errorf("finalize failed")).Build()
|
|
defer mockAppend.UnPatch()
|
|
manifestPath, err := ExecuteFunctionsForSegment(ctx, schema, nil, "parquet", storageConfig, s.collectionID, 3000, basePath, "cluster")
|
|
s.Error(err)
|
|
s.Empty(manifestPath)
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestTaskAccessorsAndCloneHelpers() {
|
|
var nilCtx context.Context
|
|
s.NoError(ensureContext(nilCtx))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
s.Error(ensureContext(ctx))
|
|
|
|
taskCtx := context.Background()
|
|
task := NewRefreshExternalCollectionTask(taskCtx, &datapb.RefreshExternalCollectionTaskRequest{TaskID: 10})
|
|
task.updatedSegments = []*datapb.SegmentInfo{{ID: 100}}
|
|
s.Equal(taskCtx, task.Ctx())
|
|
s.Equal(task.updatedSegments, task.GetUpdatedSegments())
|
|
|
|
segments := []*datapb.SegmentInfo{{ID: 1}, nil, {ID: 2}}
|
|
cloned := cloneSegments(segments)
|
|
s.Require().Len(cloned, 3)
|
|
s.Equal(int64(1), cloned[0].GetID())
|
|
s.Nil(cloned[1])
|
|
s.NotSame(segments[0], cloned[0])
|
|
cloned[0].ID = 10
|
|
s.Equal(int64(1), segments[0].GetID())
|
|
|
|
info := &TaskInfo{
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
CollID: 1000,
|
|
KeptSegments: []int64{1, 2},
|
|
UpdatedSegments: []*datapb.SegmentInfo{{ID: 3}},
|
|
}
|
|
infoClone := info.Clone()
|
|
s.Equal(info.KeptSegments, infoClone.KeptSegments)
|
|
s.Equal(info.UpdatedSegments[0].GetID(), infoClone.UpdatedSegments[0].GetID())
|
|
infoClone.KeptSegments[0] = 100
|
|
infoClone.UpdatedSegments[0].ID = 300
|
|
s.Equal(int64(1), info.KeptSegments[0])
|
|
s.Equal(int64(3), info.UpdatedSegments[0].GetID())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteWithMockedSteps() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 3000, End: 4000},
|
|
CurrentSegments: []*datapb.SegmentInfo{{ID: 1}},
|
|
ExploreManifestPath: "manifest",
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
Schema: &schemapb.CollectionSchema{Fields: []*schemapb.FieldSchema{{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar}}},
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
NumSegmentsExpected: 1,
|
|
FileIndexBegin: 0,
|
|
FileIndexEnd: 1,
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"text"}
|
|
fragments := []packed.Fragment{{FragmentID: 1, FilePath: "file", StartRow: 0, EndRow: 10, RowCount: 10}}
|
|
segmentFragments := packed.SegmentFragments{1: fragments}
|
|
updated := []*datapb.SegmentInfo{{ID: 1}}
|
|
|
|
mockFetch := mockey.Mock(mockey.GetMethod(task, "fetchFragmentsFromExternalSource")).Return(fragments, nil).Build()
|
|
defer mockFetch.UnPatch()
|
|
mockBuild := mockey.Mock(mockey.GetMethod(task, "buildCurrentSegmentFragments")).Return(segmentFragments, nil).Build()
|
|
defer mockBuild.UnPatch()
|
|
mockOrganize := mockey.Mock(mockey.GetMethod(task, "organizeSegments")).
|
|
To(func(ctx context.Context, currentSegmentFragments packed.SegmentFragments, newFragments []packed.Fragment) ([]*datapb.SegmentInfo, error) {
|
|
task.updatedSegments = updated
|
|
return updated, nil
|
|
}).Build()
|
|
defer mockOrganize.UnPatch()
|
|
|
|
err := task.Execute(ctx)
|
|
s.NoError(err)
|
|
s.Equal(req.GetPreAllocatedSegmentIds(), task.preallocatedIDRange)
|
|
s.Equal(int64(3000), task.nextAllocID)
|
|
s.Equal(updated, task.GetUpdatedSegments())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestExecuteErrorPathsWithMockedSteps() {
|
|
ctx := context.Background()
|
|
newTask := func() *RefreshExternalCollectionTask {
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 3000, End: 4000},
|
|
}
|
|
return NewRefreshExternalCollectionTask(ctx, req)
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, &datapb.RefreshExternalCollectionTaskRequest{TaskID: s.taskID})
|
|
s.Error(task.Execute(ctx))
|
|
|
|
task = newTask()
|
|
mockFetch := mockey.Mock(mockey.GetMethod(task, "fetchFragmentsFromExternalSource")).Return(nil, fmt.Errorf("fetch failed")).Build()
|
|
err := task.Execute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "failed to fetch fragments")
|
|
mockFetch.UnPatch()
|
|
|
|
task = newTask()
|
|
fragments := []packed.Fragment{{FragmentID: 1}}
|
|
mockFetch = mockey.Mock(mockey.GetMethod(task, "fetchFragmentsFromExternalSource")).Return(fragments, nil).Build()
|
|
mockBuild := mockey.Mock(mockey.GetMethod(task, "buildCurrentSegmentFragments")).Return(nil, fmt.Errorf("build failed")).Build()
|
|
err = task.Execute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "failed to build current segment fragments")
|
|
mockBuild.UnPatch()
|
|
mockFetch.UnPatch()
|
|
|
|
task = newTask()
|
|
mockFetch = mockey.Mock(mockey.GetMethod(task, "fetchFragmentsFromExternalSource")).Return(fragments, nil).Build()
|
|
mockBuild = mockey.Mock(mockey.GetMethod(task, "buildCurrentSegmentFragments")).Return(packed.SegmentFragments{}, nil).Build()
|
|
mockOrganize := mockey.Mock(mockey.GetMethod(task, "organizeSegments")).Return(nil, fmt.Errorf("organize failed")).Build()
|
|
err = task.Execute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "organize failed")
|
|
mockOrganize.UnPatch()
|
|
mockBuild.UnPatch()
|
|
mockFetch.UnPatch()
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPostExecuteAndOrganizeCanceled() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
task := NewRefreshExternalCollectionTask(ctx, &datapb.RefreshExternalCollectionTaskRequest{
|
|
TaskID: s.taskID,
|
|
CollectionID: s.collectionID,
|
|
})
|
|
s.Error(task.PostExecute(ctx))
|
|
|
|
segments, err := task.organizeSegments(ctx, nil, nil)
|
|
s.Error(err)
|
|
s.Nil(segments)
|
|
|
|
segments, err = task.balanceFragmentsToSegments(ctx, []packed.Fragment{{FragmentID: 1, RowCount: 1}})
|
|
s.Error(err)
|
|
s.Nil(segments)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBuildCurrentSegmentFragments() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
CurrentSegments: []*datapb.SegmentInfo{{ID: 1}},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.columns = []string{"text_col"}
|
|
expected := packed.SegmentFragments{1: []packed.Fragment{{FragmentID: 1}}}
|
|
var gotColumns []string
|
|
mockBuild := mockey.Mock(packed.BuildCurrentSegmentFragments).
|
|
To(func(
|
|
segments []*datapb.SegmentInfo,
|
|
storageConfig *indexpb.StorageConfig,
|
|
columns []string,
|
|
) (packed.SegmentFragments, error) {
|
|
gotColumns = columns
|
|
return expected, nil
|
|
}).Build()
|
|
defer mockBuild.UnPatch()
|
|
|
|
got, err := task.buildCurrentSegmentFragments()
|
|
s.NoError(err)
|
|
s.Equal(expected, got)
|
|
s.Equal([]string{"text_col"}, gotColumns)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegmentsWithFunctions() {
|
|
paramtable.Init()
|
|
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
PartitionID: 2000,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 3000, End: 4000},
|
|
StorageConfig: &indexpb.StorageConfig{RootPath: "files", StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Type: schemapb.FunctionType_BM25, InputFieldIds: []int64{100}, OutputFieldIds: []int64{101}},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
var gotBasePath string
|
|
mockExec := mockey.Mock(ExecuteFunctionsForSegment).
|
|
To(func(
|
|
ctx context.Context,
|
|
schema *schemapb.CollectionSchema,
|
|
fragments []packed.Fragment,
|
|
format string,
|
|
storageConfig *indexpb.StorageConfig,
|
|
collectionID int64,
|
|
segmentID int64,
|
|
basePath string,
|
|
clusterID string,
|
|
) (string, error) {
|
|
gotBasePath = basePath
|
|
return packed.MarshalManifestPath(basePath, 42), nil
|
|
}).Build()
|
|
defer mockExec.UnPatch()
|
|
mockSample := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 64}, nil).Build()
|
|
defer mockSample.UnPatch()
|
|
|
|
result, err := task.balanceFragmentsToSegments(ctx, []packed.Fragment{{FragmentID: 1, RowCount: 10}})
|
|
s.NoError(err)
|
|
s.Require().Len(result, 1)
|
|
s.Equal("files/insert_log/1000/2000/3000", gotBasePath)
|
|
s.Equal(int64(3000), result[0].GetID())
|
|
s.NotZero(result[0].GetBinlogs()[0].GetBinlogs()[0].GetMemorySize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) makeStringRecord(name string, values []string) arrow.Record {
|
|
arrowSchema := arrow.NewSchema([]arrow.Field{{Name: name, Type: arrow.BinaryTypes.String}}, nil)
|
|
builder := array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
|
|
defer builder.Release()
|
|
stringBuilder := builder.Field(0).(*array.StringBuilder)
|
|
stringBuilder.AppendValues(values, nil)
|
|
return builder.NewRecord()
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) makeStringStorageRecord(fieldID int64, name string, values []string) storage.Record {
|
|
return storage.NewSimpleArrowRecord(s.makeStringRecord(name, values), map[storage.FieldID]int{
|
|
fieldID: 0,
|
|
})
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreAllocatedSegmentIDs() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Create request with pre-allocated IDs
|
|
idRange := &datapb.IDRange{
|
|
Begin: 1000,
|
|
End: 2000,
|
|
}
|
|
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: idRange,
|
|
NumSegmentsExpected: 1000,
|
|
CurrentSegments: []*datapb.SegmentInfo{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// Before Execute(), pre-allocated fields should not be initialized
|
|
s.Nil(task.preallocatedIDRange)
|
|
s.Equal(int64(0), task.nextAllocID)
|
|
|
|
// After testing with pre-allocated IDs manually set (simulating Execute)
|
|
task.preallocatedIDRange = idRange
|
|
task.nextAllocID = idRange.Begin
|
|
|
|
// Verify pre-allocated IDs are properly initialized
|
|
s.NotNil(task.preallocatedIDRange)
|
|
s.Equal(int64(1000), task.preallocatedIDRange.Begin)
|
|
s.Equal(int64(2000), task.preallocatedIDRange.End)
|
|
s.Equal(int64(1000), task.nextAllocID)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreAllocatedIDAllocation() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Create request with pre-allocated IDs
|
|
idRange := &datapb.IDRange{
|
|
Begin: 5000,
|
|
End: 5010, // Only 10 IDs available
|
|
}
|
|
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: idRange,
|
|
NumSegmentsExpected: 10,
|
|
CurrentSegments: []*datapb.SegmentInfo{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// Manually initialize (simulating Execute)
|
|
task.preallocatedIDRange = idRange
|
|
task.nextAllocID = idRange.Begin
|
|
|
|
// Simulate creating multiple segments by advancing nextAllocID
|
|
// This should work fine up to the limit
|
|
for i := 0; i < 10; i++ {
|
|
s.True(task.nextAllocID < task.preallocatedIDRange.End, "Should have IDs available")
|
|
task.nextAllocID++
|
|
}
|
|
|
|
// Now we should be out of IDs
|
|
s.Equal(task.preallocatedIDRange.End, task.nextAllocID)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestMissingPreAllocatedIDs() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Create request WITHOUT pre-allocated IDs
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
CurrentSegments: []*datapb.SegmentInfo{},
|
|
// PreAllocatedSegmentIds is nil
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
|
|
// Execute should fail because pre-allocated IDs are missing
|
|
err := task.Execute(ctx)
|
|
s.NotNil(err)
|
|
s.Contains(err.Error(), "pre-allocated segment IDs not provided")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecute_NilSchema() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
// Schema is nil
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "schema is nil")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecute_NilStorageConfig() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
Schema: &schemapb.CollectionSchema{},
|
|
// StorageConfig is nil
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "storage config is nil")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecute_EmptyExternalSource() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
// ExternalSource is empty
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "external source is empty")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecute_InvalidExternalSpec() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
ExternalSpec: "not-valid-json",
|
|
Schema: &schemapb.CollectionSchema{},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "failed to parse external spec")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestPreExecute_UnsupportedFormat() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "test_source",
|
|
ExternalSpec: `{"format":"avro"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
err := task.PreExecute(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "unsupported format")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestParseExternalSpec() {
|
|
// Empty spec defaults to parquet
|
|
spec, err := externalspec.ParseExternalSpec("")
|
|
s.NoError(err)
|
|
s.Equal("parquet", spec.Format)
|
|
|
|
// Lance-table format
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"lance-table"}`)
|
|
s.NoError(err)
|
|
s.Equal("lance-table", spec.Format)
|
|
|
|
// Vortex format
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"vortex"}`)
|
|
s.NoError(err)
|
|
s.Equal("vortex", spec.Format)
|
|
|
|
// Missing format defaults to parquet
|
|
spec, err = externalspec.ParseExternalSpec(`{}`)
|
|
s.NoError(err)
|
|
s.Equal("parquet", spec.Format)
|
|
|
|
// Invalid JSON
|
|
_, err = externalspec.ParseExternalSpec("not-json")
|
|
s.Error(err)
|
|
|
|
// Unsupported format
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"avro"}`)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "unsupported format")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestParseExternalSpec_ExtfsWhitelist() {
|
|
// Valid extfs keys
|
|
spec, err := externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_iam":"true","region":"us-west-2"}}`)
|
|
s.NoError(err)
|
|
s.Equal("true", spec.Extfs["use_iam"])
|
|
s.Equal("us-west-2", spec.Extfs["region"])
|
|
|
|
// All allowed keys should be accepted
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_ssl":"false","use_virtual_host":"false","cloud_provider":"aws","iam_endpoint":"https://sts.amazonaws.com","storage_type":"remote","ssl_ca_cert":"cert"}}`)
|
|
s.NoError(err)
|
|
s.Len(spec.Extfs, 6)
|
|
|
|
// Credential keys are now allowed for cross-bucket scenarios
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"access_key_id":"AKIA..."}}`)
|
|
s.NoError(err)
|
|
s.Equal("AKIA...", spec.Extfs["access_key_id"])
|
|
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"access_key_value":"secret"}}`)
|
|
s.NoError(err)
|
|
s.Equal("secret", spec.Extfs["access_key_value"])
|
|
|
|
// Unknown key
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"unknown_key":"val"}}`)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "not allowed")
|
|
|
|
// Empty extfs is fine
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{}}`)
|
|
s.NoError(err)
|
|
s.Empty(spec.Extfs)
|
|
|
|
// No extfs field is fine
|
|
spec, err = externalspec.ParseExternalSpec(`{"format":"parquet"}`)
|
|
s.NoError(err)
|
|
s.Nil(spec.Extfs)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestParseExternalSpec_ExtfsBooleanValidation() {
|
|
// Valid boolean values
|
|
_, err := externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_iam":"true"}}`)
|
|
s.NoError(err)
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_ssl":"false"}}`)
|
|
s.NoError(err)
|
|
|
|
// Invalid boolean values
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_iam":"maybe"}}`)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "must be \"true\" or \"false\"")
|
|
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_ssl":"1"}}`)
|
|
s.Error(err)
|
|
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"use_virtual_host":"yes"}}`)
|
|
s.Error(err)
|
|
|
|
// Non-boolean keys accept any string value
|
|
_, err = externalspec.ParseExternalSpec(`{"format":"parquet","extfs":{"region":"any-value-is-ok"}}`)
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFragmentKey() {
|
|
f1 := packed.Fragment{FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000}
|
|
f2 := packed.Fragment{FilePath: "/data/file1.parquet", StartRow: 1000, EndRow: 2000}
|
|
f3 := packed.Fragment{FilePath: "/data/file2.parquet", StartRow: 0, EndRow: 1000}
|
|
|
|
// Same file, different ranges should produce different keys
|
|
s.NotEqual(fragmentKey(f1), fragmentKey(f2))
|
|
|
|
// Different files, same ranges should produce different keys
|
|
s.NotEqual(fragmentKey(f1), fragmentKey(f3))
|
|
|
|
// Identical fragments should produce the same key
|
|
f4 := packed.Fragment{FilePath: "/data/file1.parquet", StartRow: 0, EndRow: 1000}
|
|
s.Equal(fragmentKey(f1), fragmentKey(f4))
|
|
|
|
// L0 deltalogs are not part of the L1 data identity. They are handled by a
|
|
// manifest-only refresh so the target segment ID can be reused.
|
|
f5 := packed.Fragment{
|
|
FilePath: "/data/file1.parquet",
|
|
StartRow: 0,
|
|
EndRow: 1000,
|
|
Deltalogs: []*datapb.FieldBinlog{{
|
|
FieldID: 100,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
LogPath: "s3://bucket/files/insert_log/1/_delta/10",
|
|
EntriesNum: 2,
|
|
}},
|
|
}},
|
|
}
|
|
f6 := packed.Fragment{
|
|
FilePath: "/data/file1.parquet",
|
|
StartRow: 0,
|
|
EndRow: 1000,
|
|
Deltalogs: []*datapb.FieldBinlog{{
|
|
FieldID: 100,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 11,
|
|
LogPath: "s3://bucket/files/insert_log/1/_delta/11",
|
|
EntriesNum: 2,
|
|
}},
|
|
}},
|
|
}
|
|
s.Equal(fragmentKey(f1), fragmentKey(f5))
|
|
s.Equal(fragmentKey(f5), fragmentKey(f6))
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestGetColumnNamesFromSchema() {
|
|
// Nil schema
|
|
columns := packed.GetColumnNamesFromSchema(nil)
|
|
s.Nil(columns)
|
|
|
|
// Schema with ExternalField set
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{Name: "id", ExternalField: "external_id"},
|
|
{Name: "vector", ExternalField: "embedding"},
|
|
},
|
|
}
|
|
columns = packed.GetColumnNamesFromSchema(schema)
|
|
s.Equal([]string{"external_id", "embedding"}, columns)
|
|
|
|
// Schema without ExternalField (fallback to field name)
|
|
schema2 := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{Name: "id"},
|
|
{Name: "vector"},
|
|
},
|
|
}
|
|
columns = packed.GetColumnNamesFromSchema(schema2)
|
|
s.Equal([]string{"id", "vector"}, columns)
|
|
|
|
// Mixed: some with ExternalField, some without
|
|
schema3 := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{Name: "id", ExternalField: "external_id"},
|
|
{Name: "vector"},
|
|
},
|
|
}
|
|
columns = packed.GetColumnNamesFromSchema(schema3)
|
|
s.Equal([]string{"external_id", "vector"}, columns)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFetchFragmentsFromExternalSource_EmptyManifest() {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "s3:///bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
ExploreManifestPath: "", // empty
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
_, err := task.fetchFragmentsFromExternalSource(ctx)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "manifest path is required")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestFetchFragmentsFromExternalSource_Success() {
|
|
paramtable.Init()
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
ExternalSource: "s3:///bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
ExploreManifestPath: "/manifests/explore.json",
|
|
FileIndexBegin: 0,
|
|
FileIndexEnd: 5,
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local", BucketName: "/tmp"},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"col1"}
|
|
|
|
mockFetch := mockey.Mock(packed.FetchFragmentsFromExternalSourceWithRange).
|
|
Return([]packed.Fragment{
|
|
{FragmentID: 0, FilePath: "f1.parquet", StartRow: 0, EndRow: 1000, RowCount: 1000},
|
|
}, nil).Build()
|
|
defer mockFetch.UnPatch()
|
|
|
|
frags, err := task.fetchFragmentsFromExternalSource(ctx)
|
|
s.NoError(err)
|
|
s.Len(frags, 1)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_CreateManifestError() {
|
|
paramtable.Init()
|
|
ctx := context.Background()
|
|
tmpDir := s.T().TempDir()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
ExternalSource: "s3:///bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local", BucketName: tmpDir, RootPath: tmpDir},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"col1"}
|
|
task.preallocatedIDRange = &datapb.IDRange{Begin: 1, End: 100}
|
|
task.nextAllocID = 1
|
|
|
|
// Mock createManifestForSegment to return error
|
|
mockCM := mockey.Mock((*RefreshExternalCollectionTask).createManifestForSegment).
|
|
Return("", fmt.Errorf("create manifest failed")).Build()
|
|
defer mockCM.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 0, FilePath: "f.parquet", RowCount: 100, StartRow: 0, EndRow: 100},
|
|
}
|
|
_, err := task.balanceFragmentsToSegments(ctx, fragments)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "create manifest failed")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ContextCanceled() {
|
|
paramtable.Init()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
cancel()
|
|
tmpDir := s.T().TempDir()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
ExternalSource: "s3:///bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local", BucketName: tmpDir, RootPath: tmpDir},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"col1"}
|
|
task.preallocatedIDRange = &datapb.IDRange{Begin: 1, End: 100}
|
|
task.nextAllocID = 1
|
|
|
|
// createManifestForSegment should not be called because ctx is canceled
|
|
mockCM := mockey.Mock((*RefreshExternalCollectionTask).createManifestForSegment).
|
|
Return("/manifest", nil).Build()
|
|
defer mockCM.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 0, FilePath: "f.parquet", RowCount: 100, StartRow: 0, EndRow: 100},
|
|
}
|
|
_, err := task.balanceFragmentsToSegments(ctx, fragments)
|
|
s.Error(err)
|
|
}
|
|
|
|
// TestBalanceFragmentsToSegments_CtxCancelledDuringManifest verifies the
|
|
// post-AwaitAll ctx.Err() guard: if workers finish without error (e.g. a mock
|
|
// that ignores ctx) but the context was canceled during the pool run, the
|
|
// function must still return ctx.Err() rather than build a result from the
|
|
// half-done work.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_CtxCancelledDuringManifest() {
|
|
paramtable.Init()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
tmpDir := s.T().TempDir()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
ExternalSource: "s3:///bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local", BucketName: tmpDir, RootPath: tmpDir},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
task.columns = []string{"col1"}
|
|
task.preallocatedIDRange = &datapb.IDRange{Begin: 1, End: 100}
|
|
task.nextAllocID = 1
|
|
|
|
// Mock createManifestForSegment to cancel the ctx mid-run and return nil.
|
|
// Workers do not propagate an error, so AwaitAll returns nil; the
|
|
// post-AwaitAll ctx.Err() check is the only thing that surfaces the cancel.
|
|
mockCM := mockey.Mock((*RefreshExternalCollectionTask).createManifestForSegment).
|
|
To(func(_ *RefreshExternalCollectionTask, _ context.Context, _ int64, _ []packed.Fragment) (string, error) {
|
|
cancel()
|
|
return "/manifest", nil
|
|
}).Build()
|
|
defer mockCM.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 0, FilePath: "f.parquet", RowCount: 100, StartRow: 0, EndRow: 100},
|
|
}
|
|
_, err := task.balanceFragmentsToSegments(ctx, fragments)
|
|
s.Require().Error(err)
|
|
s.ErrorIs(err, context.Canceled)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBuildFakeBinlogs() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 1, Name: "pk"},
|
|
{FieldID: 100, Name: "text"},
|
|
{FieldID: 101, Name: "vec"},
|
|
},
|
|
}
|
|
|
|
binlogs := buildFakeBinlogs(999, 1000, 512000, schema, "parquet")
|
|
s.Len(binlogs, 1)
|
|
fb := binlogs[0]
|
|
s.Equal(int64(0), fb.GetFieldID(), "should use DefaultShortColumnGroupID=0")
|
|
s.ElementsMatch([]int64{1, 100, 101}, fb.GetChildFields())
|
|
s.Equal("parquet", fb.GetFormat())
|
|
s.Len(fb.GetBinlogs(), 1)
|
|
s.Equal(int64(999), fb.GetBinlogs()[0].GetLogID(), "logID should be segmentID")
|
|
s.Equal(int64(1000), fb.GetBinlogs()[0].GetEntriesNum())
|
|
s.Equal(int64(512000), fb.GetBinlogs()[0].GetMemorySize())
|
|
s.Equal(int64(512000), fb.GetBinlogs()[0].GetLogSize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBuildFakeBinlogs_NilSchema() {
|
|
binlogs := buildFakeBinlogs(888, 500, 100000, nil, "vortex")
|
|
s.Len(binlogs, 1)
|
|
s.Empty(binlogs[0].GetChildFields())
|
|
s.Equal("vortex", binlogs[0].GetFormat())
|
|
s.Equal(int64(500), binlogs[0].GetBinlogs()[0].GetEntriesNum())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSumFieldSizes() {
|
|
schema := &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 1, Name: "pk"}, // no ExternalField
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"}, // external
|
|
{FieldID: 101, Name: "vec", ExternalField: "embedding_col"}, // external
|
|
},
|
|
}
|
|
|
|
fieldSizes := map[string]int64{
|
|
"text_col": 64,
|
|
"embedding_col": 512,
|
|
"extra_col": 999, // not in schema, should be ignored
|
|
}
|
|
|
|
total := sumFieldSizes(fieldSizes, schema)
|
|
s.Equal(int64(576), total, "should sum only external fields: 64+512")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSumFieldSizes_MilvusTableUsesSourceFieldIDs() {
|
|
schema := &schemapb.CollectionSchema{
|
|
ExternalSpec: `{"format":"milvus-table"}`,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 0, Name: common.VirtualPKFieldName},
|
|
{FieldID: 100, Name: "target_pk", ExternalField: "pk"},
|
|
{FieldID: 101, Name: "target_vec", ExternalField: "vec"},
|
|
},
|
|
}
|
|
fieldSizes := map[string]int64{
|
|
"100": 64,
|
|
"101": 512,
|
|
}
|
|
|
|
total := sumFieldSizes(fieldSizes, schema)
|
|
s.Equal(int64(576), total)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSumFieldSizes_MilvusTableSkipsFunctionOutputField() {
|
|
schema := &schemapb.CollectionSchema{
|
|
ExternalSpec: `{"format":"milvus-table"}`,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text"},
|
|
{FieldID: 101, Name: "vec", ExternalField: "vec"},
|
|
{FieldID: 102, Name: "sparse", DataType: schemapb.DataType_SparseFloatVector, IsFunctionOutput: true},
|
|
},
|
|
}
|
|
fieldSizes := map[string]int64{
|
|
"100": 64,
|
|
"101": 512,
|
|
"102": 4096,
|
|
}
|
|
|
|
total := sumFieldSizes(fieldSizes, schema)
|
|
s.Equal(int64(576), total)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSumFieldSizes_NilSchema() {
|
|
fieldSizes := map[string]int64{"a": 10, "b": 20}
|
|
total := sumFieldSizes(fieldSizes, nil)
|
|
s.Equal(int64(30), total, "nil schema should sum all fields")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_SamplingFails() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
// Sampling fails on the only manifest. The previous behavior silently
|
|
// emitted MemorySize=0 segments, which collapsed QueryNode's resource
|
|
// estimator and risked OOM on load. Current behavior is to fail the
|
|
// task so the task-level retry path can try again with a clean state.
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(nil, fmt.Errorf("sampling error")).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err, "sampling failure must surface as a task error, not a silent MemorySize=0")
|
|
s.Contains(err.Error(), "sampling failed")
|
|
s.Nil(result)
|
|
}
|
|
|
|
// Regression for #48637: when external_field mappings point to columns
|
|
// absent in the parquet file, SampleExternalFieldSizes returns a real
|
|
// error like "Column 'wrong_col_a' not found in schema". That error
|
|
// must propagate to the task error (and thus to the RefreshFailed
|
|
// reason surfaced to the client) rather than being swallowed as a
|
|
// generic "sampling failed for all N segment(s)" message.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_SamplingError_RootCausePropagated() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "wrong_col_a"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
cppErr := fmt.Errorf("Invalid: Column 'wrong_col_a' not found in schema. [path=data.parquet]")
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).Return(nil, cppErr).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{{FragmentID: 1, RowCount: 500}}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
// Root cause string must reach the client-facing task error.
|
|
s.Contains(err.Error(), "Column 'wrong_col_a' not found in schema")
|
|
// And emit actionable hint guiding the user to the real fix.
|
|
s.Contains(err.Error(), "external_field mappings")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_SamplingTypeMismatchFails() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{
|
|
FieldID: 100,
|
|
Name: "age",
|
|
DataType: schemapb.DataType_Int8,
|
|
ExternalField: "age_col",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
typeMismatchErr := fmt.Errorf("field type mismatch, expected Arrow int8, actual Arrow int64")
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).Return(nil, typeMismatchErr).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{{FragmentID: 1, RowCount: 500}}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err)
|
|
s.Nil(result)
|
|
s.Contains(err.Error(), "sampling failed")
|
|
s.Contains(err.Error(), "field type mismatch")
|
|
s.Contains(err.Error(), "expected Arrow int8")
|
|
s.Contains(err.Error(), "actual Arrow int64")
|
|
}
|
|
|
|
// Regression: the samplePerSegment=true branch of Phase 3 was previously
|
|
// uncovered by tests. This exercise is three parts:
|
|
// 1. All per-segment samples succeed → each segment gets its own
|
|
// distinct MemorySize (proving per-segment semantics actually take
|
|
// effect rather than collapsing to the first value).
|
|
// 2. First segment sample fails, later segments succeed → the zero-fill
|
|
// pass retroactively backfills the first slot with the first
|
|
// successful average (fallbackAvg).
|
|
// 3. All per-segment samples fail → task fails (no silent MemorySize=0).
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_PerSegmentSampling_AllSucceed() {
|
|
paramtable.Init()
|
|
key := paramtable.Get().QueryNodeCfg.ExternalCollectionSamplePerSegment.Key
|
|
paramtable.Get().Save(key, "true")
|
|
defer paramtable.Get().Reset(key)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
// Return distinct sizes per call so we can prove per-segment samples
|
|
// actually land in distinct slots.
|
|
var callCount int32
|
|
perCallSizes := []int64{50, 100, 200}
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
To(func(manifestPath string, rows int, collectionID int64, externalSource, externalSpec string, schema *schemapb.CollectionSchema, storageConfig *indexpb.StorageConfig) (map[string]int64, error) {
|
|
idx := int(atomic.AddInt32(&callCount, 1)) - 1
|
|
if idx >= len(perCallSizes) {
|
|
idx = len(perCallSizes) - 1
|
|
}
|
|
return map[string]int64{"text_col": perCallSizes[idx]}, nil
|
|
}).Build()
|
|
defer m2.UnPatch()
|
|
|
|
// 3 fragments each above targetRowsPerSegment → 3 segments.
|
|
targetRows := paramtable.Get().DataNodeCfg.ExternalCollectionTargetRowsPerSegment.GetAsInt64()
|
|
rowsPerFragment := targetRows * 2 // force one segment per fragment
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: rowsPerFragment},
|
|
{FragmentID: 2, RowCount: rowsPerFragment},
|
|
{FragmentID: 3, RowCount: rowsPerFragment},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 3, "expected 3 segments for 3 oversized fragments")
|
|
|
|
// Each segment's MemorySize reflects its own sample (size * rowCount).
|
|
// SampleExternalFieldSizes is called per-segment in manifest order.
|
|
for i, seg := range result {
|
|
expectedSize := perCallSizes[i] * rowsPerFragment
|
|
actual := seg.GetBinlogs()[0].GetBinlogs()[0].GetMemorySize()
|
|
s.Equal(expectedSize, actual, "segment %d MemorySize should match its own sample", i)
|
|
}
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_PerSegmentSampling_FirstFailBackfills() {
|
|
paramtable.Init()
|
|
key := paramtable.Get().QueryNodeCfg.ExternalCollectionSamplePerSegment.Key
|
|
paramtable.Get().Save(key, "true")
|
|
defer paramtable.Get().Reset(key)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
// First call fails; second and third succeed. Verify the first slot
|
|
// is retroactively backfilled with the first successful average (150),
|
|
// not the third (250), since fallbackAvg latches on the first success.
|
|
var callCount int32
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
To(func(manifestPath string, rows int, collectionID int64, externalSource, externalSpec string, schema *schemapb.CollectionSchema, storageConfig *indexpb.StorageConfig) (map[string]int64, error) {
|
|
idx := int(atomic.AddInt32(&callCount, 1)) - 1
|
|
switch idx {
|
|
case 0:
|
|
return nil, fmt.Errorf("transient sample failure")
|
|
case 1:
|
|
return map[string]int64{"text_col": 150}, nil
|
|
default:
|
|
return map[string]int64{"text_col": 250}, nil
|
|
}
|
|
}).Build()
|
|
defer m2.UnPatch()
|
|
|
|
targetRows := paramtable.Get().DataNodeCfg.ExternalCollectionTargetRowsPerSegment.GetAsInt64()
|
|
rowsPerFragment := targetRows * 2
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: rowsPerFragment},
|
|
{FragmentID: 2, RowCount: rowsPerFragment},
|
|
{FragmentID: 3, RowCount: rowsPerFragment},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 3)
|
|
|
|
// Expected: [150*rows, 150*rows, 250*rows]
|
|
// - segment 0: backfilled from fallbackAvg=150
|
|
// - segment 1: native sample 150
|
|
// - segment 2: native sample 250
|
|
s.Equal(int64(150)*rowsPerFragment, result[0].GetBinlogs()[0].GetBinlogs()[0].GetMemorySize(), "segment 0 should be backfilled from fallbackAvg")
|
|
s.Equal(int64(150)*rowsPerFragment, result[1].GetBinlogs()[0].GetBinlogs()[0].GetMemorySize())
|
|
s.Equal(int64(250)*rowsPerFragment, result[2].GetBinlogs()[0].GetBinlogs()[0].GetMemorySize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_PerSegmentSampling_AllFail() {
|
|
paramtable.Init()
|
|
key := paramtable.Get().QueryNodeCfg.ExternalCollectionSamplePerSegment.Key
|
|
paramtable.Get().Save(key, "true")
|
|
defer paramtable.Get().Reset(key)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(nil, fmt.Errorf("all samples failed")).Build()
|
|
defer m2.UnPatch()
|
|
|
|
targetRows := paramtable.Get().DataNodeCfg.ExternalCollectionTargetRowsPerSegment.GetAsInt64()
|
|
rowsPerFragment := targetRows * 2
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: rowsPerFragment},
|
|
{FragmentID: 2, RowCount: rowsPerFragment},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err, "all per-segment samples failing must surface as a task error")
|
|
s.Contains(err.Error(), "sampling failed")
|
|
s.Nil(result)
|
|
}
|
|
|
|
// Regression: if the schema has no mapped external data fields, sumFieldSizes
|
|
// returns 0 even when SampleExternalFieldSizes itself "succeeds". The old
|
|
// code treated this as a successful zero-sized sample and wrote MemorySize=0
|
|
// fake binlogs into every segment, which feeds QueryNode a degenerate
|
|
// EstimatedBytesPerRow=0 and risks OOM on load. Current behavior: fail the
|
|
// task.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ZeroSumTreatedAsFailure() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
// Schema with NO ExternalField set — sumFieldSizes returns 0.
|
|
{FieldID: 100, Name: "native_only"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
// Sampling "succeeds" but returns sizes for a column the schema
|
|
// doesn't reference — sumFieldSizes totals to 0.
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"orphan_col": 128}, nil).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err, "zero-sum sample must surface as a task error")
|
|
s.Contains(err.Error(), "sampling failed")
|
|
s.Nil(result)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestSumFieldSizes_MilvusTableRealPKIncludesSourceTimestamp() {
|
|
schema := &schemapb.CollectionSchema{
|
|
ExternalSpec: `{"format":"milvus-table"}`,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
|
|
{FieldID: 101, Name: "vec", DataType: schemapb.DataType_FloatVector},
|
|
},
|
|
}
|
|
|
|
total := sumFieldSizes(map[string]int64{
|
|
"100": 16,
|
|
"101": 64,
|
|
"1": 8,
|
|
}, schema)
|
|
|
|
s.Equal(int64(88), total)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_InsufficientIDs() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 101}, // only 1 ID, need 2
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
_, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "insufficient pre-allocated IDs")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ManifestCreateFails() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
Return("", fmt.Errorf("storage error")).Build()
|
|
defer m1.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
_, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "failed to create manifest")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ContextCancelDuringManifest() {
|
|
paramtable.Init()
|
|
|
|
// Use a real cancelable context that we pass to balanceFragmentsToSegments
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
// Mock manifest creation: first call cancels ctx and returns error
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(_ context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
cancel() // cancel the ctx we pass to balanceFragmentsToSegments
|
|
return "", fmt.Errorf("canceled")
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
// Use multiple fragments so there are multiple works — ctx cancel is checked
|
|
// in the task dispatch loop (L643) and after wg.Wait (L657)
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 300},
|
|
{FragmentID: 2, RowCount: 400},
|
|
{FragmentID: 3, RowCount: 500},
|
|
}
|
|
|
|
// Pass the cancelable ctx so L643 `ctx.Err()` and L657 `ctx.Err()` can trigger
|
|
_, err := task.balanceFragmentsToSegments(ctx, fragments)
|
|
s.Error(err)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_EnsureContextInLoop() {
|
|
paramtable.Init()
|
|
|
|
// Pre-cancel the context to trigger ensureContext inside for loops (L517, L558)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 100},
|
|
{FragmentID: 2, RowCount: 200},
|
|
}
|
|
|
|
// Cancel immediately — the first ensureContext (L508) passes because it checks
|
|
// before cancel. We need to cancel between L508 and L517.
|
|
// Use a goroutine trick: mock ensureContext behavior isn't possible directly,
|
|
// so just cancel and pass the canceled ctx.
|
|
cancel()
|
|
_, err := task.balanceFragmentsToSegments(ctx, fragments)
|
|
s.ErrorIs(err, context.Canceled)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_EmptyBinSkipped() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
// Non-zero sample is required now that a zero sum is
|
|
// treated as a sampling failure. The intent of this test
|
|
// is the bin-skipping behavior, not the sampling path.
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 64}, nil).Build()
|
|
defer m2.UnPatch()
|
|
|
|
// One tiny fragment but targetRowsPerSegment is large — only 1 bin used, others empty
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 100},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.Equal(int64(100), result[0].GetNumOfRows())
|
|
s.Equal(datapb.SegmentLevel_L1, result[0].GetLevel())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ContextCanceledInLoops() {
|
|
paramtable.Init()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
cancelAt int
|
|
fragments []packed.Fragment
|
|
}{
|
|
{
|
|
name: "total row loop",
|
|
cancelAt: 2,
|
|
fragments: []packed.Fragment{{FragmentID: 1, RowCount: 100}},
|
|
},
|
|
{
|
|
name: "bin packing loop",
|
|
cancelAt: 4,
|
|
fragments: []packed.Fragment{{FragmentID: 1, RowCount: 100}, {FragmentID: 2, RowCount: 200}},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.T().Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
var calls int
|
|
mockEnsure := mockey.Mock(ensureContext).To(func(ctx context.Context) error {
|
|
calls++
|
|
if calls == tc.cancelAt {
|
|
return context.Canceled
|
|
}
|
|
return nil
|
|
}).Build()
|
|
defer mockEnsure.UnPatch()
|
|
|
|
result, err := task.balanceFragmentsToSegments(ctx, tc.fragments)
|
|
s.ErrorIs(err, context.Canceled)
|
|
s.Nil(result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_ContextCanceledInWorker() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
mockEnsure := mockey.Mock(ensureContext).Return(nil).Build()
|
|
defer mockEnsure.UnPatch()
|
|
|
|
result, err := task.balanceFragmentsToSegments(ctx, []packed.Fragment{{FragmentID: 1, RowCount: 100}})
|
|
s.ErrorIs(err, context.Canceled)
|
|
s.Nil(result)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_FunctionOutputEstimateError() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{FieldID: 101, Name: "bad_output", DataType: schemapb.DataType_VarChar, IsFunctionOutput: true},
|
|
},
|
|
},
|
|
}
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
mockManifest := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
Return("manifest.json", nil).Build()
|
|
defer mockManifest.UnPatch()
|
|
mockSample := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 64}, nil).Build()
|
|
defer mockSample.UnPatch()
|
|
|
|
result, err := task.balanceFragmentsToSegments(ctx, []packed.Fragment{{FragmentID: 1, RowCount: 100}})
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "estimate function output field bad_output")
|
|
s.Nil(result)
|
|
}
|
|
|
|
// TestBalanceFragmentsToSegments_PassesStorageConfigAndSpecExtfs verifies that
|
|
// balanceFragmentsToSegments correctly passes storageConfig and specExtfs
|
|
// (built from parsedSpec) to SampleExternalFieldSizes. This is the core change
|
|
// that eliminates the dependency on C++ LoonFFIPropertiesSingleton.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestEstimateFunctionOutputBytesPerRow_NoOutputs() {
|
|
bytes, err := estimateFunctionOutputBytesPerRow(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar},
|
|
},
|
|
})
|
|
|
|
s.NoError(err)
|
|
s.Equal(int64(0), bytes)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestEstimateFunctionOutputBytesPerRow_VectorOutputs() {
|
|
bytes, err := estimateFunctionOutputBytesPerRow(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{
|
|
FieldID: 101,
|
|
Name: "float_vec",
|
|
DataType: schemapb.DataType_FloatVector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "4"}},
|
|
},
|
|
{
|
|
FieldID: 102,
|
|
Name: "int8_vec",
|
|
DataType: schemapb.DataType_Int8Vector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}},
|
|
},
|
|
{
|
|
FieldID: 103,
|
|
Name: "binary_vec",
|
|
DataType: schemapb.DataType_BinaryVector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "64"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
s.NoError(err)
|
|
s.Equal(int64(16+8+8), bytes)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestEstimateFunctionOutputBytesPerRow_ScalarOutputs() {
|
|
bytes, err := estimateFunctionOutputBytesPerRow(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 101, Name: "score", DataType: schemapb.DataType_Int64, IsFunctionOutput: true},
|
|
{
|
|
FieldID: 102,
|
|
Name: "label",
|
|
DataType: schemapb.DataType_VarChar,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "max_length", Value: "20"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
s.NoError(err)
|
|
s.Equal(int64(8+20), bytes)
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestEstimateFunctionOutputBytesPerRow_InvalidOutputField() {
|
|
_, err := estimateFunctionOutputBytesPerRow(&schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 101, Name: "bad_text", DataType: schemapb.DataType_VarChar, IsFunctionOutput: true},
|
|
},
|
|
})
|
|
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "estimate function output field bad_text")
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_AddsFunctionOutputMemorySize() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "s3://bucket/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", DataType: schemapb.DataType_VarChar, ExternalField: "text_col"},
|
|
{
|
|
FieldID: 101,
|
|
Name: "embedding",
|
|
DataType: schemapb.DataType_FloatVector,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "4"}},
|
|
},
|
|
{
|
|
FieldID: 102,
|
|
Name: "scalar_output",
|
|
DataType: schemapb.DataType_VarChar,
|
|
IsFunctionOutput: true,
|
|
TypeParams: []*commonpb.KeyValuePair{{Key: "max_length", Value: "20"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, storageConfig *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
Return(map[string]int64{"text_col": 64}, nil).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{{FragmentID: 1, RowCount: 10}}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
s.Len(result[0].GetBinlogs(), 1)
|
|
s.Len(result[0].GetBinlogs()[0].GetBinlogs(), 1)
|
|
s.Equal(int64((64+16+20)*10), result[0].GetBinlogs()[0].GetBinlogs()[0].GetMemorySize())
|
|
}
|
|
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_PassesStorageConfigAndSpecExtfs() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
expectedStorageConfig := &indexpb.StorageConfig{
|
|
StorageType: "minio",
|
|
Address: "localhost:9000",
|
|
BucketName: "test-bucket",
|
|
AccessKeyID: "minioadmin",
|
|
SecretAccessKey: "minioadmin",
|
|
}
|
|
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: expectedStorageConfig,
|
|
ExternalSource: "s3://s3.us-west-2.amazonaws.com/ext-bucket/data/",
|
|
ExternalSpec: `{"format":"parquet","extfs":{"region":"us-west-2","use_ssl":"true","cloud_provider":"aws"}}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "vec", ExternalField: "vec_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
// Parse the spec so the balance path has an in-memory struct to work with.
|
|
parsed, err := externalspec.ParseExternalSpec(req.GetExternalSpec())
|
|
s.Require().NoError(err)
|
|
task.parsedSpec = parsed
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, sc *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
var capturedStorageConfig *indexpb.StorageConfig
|
|
var capturedExternalSpec string
|
|
var capturedSchema *schemapb.CollectionSchema
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
To(func(manifestPath string, rows int, collectionID int64, externalSource, externalSpec string, schema *schemapb.CollectionSchema, storageConfig *indexpb.StorageConfig) (map[string]int64, error) {
|
|
capturedSchema = schema
|
|
capturedStorageConfig = storageConfig
|
|
capturedExternalSpec = externalSpec
|
|
return map[string]int64{"vec_col": 3072}, nil
|
|
}).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 500},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
|
|
// Verify storageConfig was passed through (not nil)
|
|
s.NotNil(capturedStorageConfig, "storageConfig must be passed to SampleExternalFieldSizes")
|
|
s.Equal(expectedStorageConfig.GetAddress(), capturedStorageConfig.GetAddress())
|
|
s.Equal(expectedStorageConfig.GetBucketName(), capturedStorageConfig.GetBucketName())
|
|
s.Equal(expectedStorageConfig.GetStorageType(), capturedStorageConfig.GetStorageType())
|
|
s.Equal(req.GetSchema(), capturedSchema, "schema must be passed to SampleExternalFieldSizes")
|
|
|
|
// Verify raw externalSpec JSON is forwarded so C++ InjectExternalSpecProperties
|
|
// can derive extfs.* overrides.
|
|
s.Equal(req.GetExternalSpec(), capturedExternalSpec)
|
|
}
|
|
|
|
// TestBalanceFragmentsToSegments_NilSpecExtfsWhenNoExtfsInSpec verifies that
|
|
// when ExternalSpec has no extfs overrides (same-bucket scenario), specExtfs
|
|
// is empty but storageConfig is still passed.
|
|
func (s *RefreshExternalCollectionTaskSuite) TestBalanceFragmentsToSegments_NilSpecExtfsWhenNoExtfsInSpec() {
|
|
paramtable.Init()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
req := &datapb.RefreshExternalCollectionTaskRequest{
|
|
CollectionID: s.collectionID,
|
|
TaskID: s.taskID,
|
|
PreAllocatedSegmentIds: &datapb.IDRange{Begin: 100, End: 200},
|
|
StorageConfig: &indexpb.StorageConfig{StorageType: "local"},
|
|
ExternalSource: "/local/data/",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
Schema: &schemapb.CollectionSchema{
|
|
Fields: []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "text", ExternalField: "text_col"},
|
|
},
|
|
},
|
|
}
|
|
|
|
task := NewRefreshExternalCollectionTask(ctx, req)
|
|
task.preallocatedIDRange = req.GetPreAllocatedSegmentIds()
|
|
task.nextAllocID = task.preallocatedIDRange.Begin
|
|
task.parsedSpec = &externalspec.ExternalSpec{Format: "parquet"}
|
|
|
|
m1 := mockey.Mock(packed.CreateSegmentManifestWithBasePathAndExtfs).
|
|
To(func(ctx context.Context, basePath, format string, columns []string, fragments []packed.Fragment, sc *indexpb.StorageConfig, extfs packed.ExternalSpecContext) (string, error) {
|
|
return fmt.Sprintf("%s/manifest.json", basePath), nil
|
|
}).Build()
|
|
defer m1.UnPatch()
|
|
|
|
var capturedStorageConfig *indexpb.StorageConfig
|
|
var capturedExternalSpec string
|
|
m2 := mockey.Mock(packed.SampleExternalFieldSizes).
|
|
To(func(manifestPath string, rows int, collectionID int64, externalSource, externalSpec string, schema *schemapb.CollectionSchema, storageConfig *indexpb.StorageConfig) (map[string]int64, error) {
|
|
capturedStorageConfig = storageConfig
|
|
capturedExternalSpec = externalSpec
|
|
return map[string]int64{"text_col": 64}, nil
|
|
}).Build()
|
|
defer m2.UnPatch()
|
|
|
|
fragments := []packed.Fragment{
|
|
{FragmentID: 1, RowCount: 100},
|
|
}
|
|
|
|
result, err := task.balanceFragmentsToSegments(context.Background(), fragments)
|
|
s.NoError(err)
|
|
s.Len(result, 1)
|
|
|
|
// storageConfig is always passed
|
|
s.NotNil(capturedStorageConfig)
|
|
s.Equal("local", capturedStorageConfig.GetStorageType())
|
|
|
|
// Same-bucket: externalSpec has no extfs → C++ inject is a no-op.
|
|
s.Equal(req.GetExternalSpec(), capturedExternalSpec)
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTaskSuite(t *testing.T) {
|
|
suite.Run(t, new(RefreshExternalCollectionTaskSuite))
|
|
}
|