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

1202 lines
46 KiB
Go

package datacoord
import (
"context"
"math"
"strconv"
"strings"
"testing"
"time"
"github.com/blang/semver/v4"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"github.com/stretchr/testify/mock"
"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/datacoord/allocator"
"github.com/milvus-io/milvus/internal/metastore/model"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/mlog"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
func TestCompactionTriggerManagerSuite(t *testing.T) {
suite.Run(t, new(CompactionTriggerManagerSuite))
}
func TestEstimateResultSegmentCount(t *testing.T) {
tests := []struct {
name string
totalSize float64
targetSize float64
want int64
}{
{name: "exact multiple", totalSize: 300, targetSize: 100, want: 3},
{name: "fractional rounds up", totalSize: 301, targetSize: 100, want: 4},
{name: "zero total size", totalSize: 0, targetSize: 100, want: 1},
{name: "zero target size", totalSize: 100, targetSize: 0, want: 1},
{name: "large ratio", totalSize: 10_000, targetSize: 64, want: 157},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := estimateResultSegmentCount(test.totalSize, test.targetSize)
if got != test.want {
t.Fatalf("estimateResultSegmentCount(%v, %v) = %d, want %d", test.totalSize, test.targetSize, got, test.want)
}
})
}
}
func TestCompactionIDBlockTakeFromMetadataTail(t *testing.T) {
t.Run("returns fixed segment ID range", func(t *testing.T) {
block := &compactionIDBlock{
segments: &datapb.IDRange{Begin: 100, End: 103},
next: 103,
end: 104,
}
planID, err := block.take()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
segmentIDRange := block.segmentIDRange()
if planID != 103 {
t.Fatalf("planID = %d, want 103", planID)
}
if segmentIDRange.GetBegin() != 100 || segmentIDRange.GetEnd() != 103 {
t.Fatalf("segment ID range = [%d, %d), want [100, 103)", segmentIDRange.GetBegin(), segmentIDRange.GetEnd())
}
})
t.Run("rejects future metadata over-consumption", func(t *testing.T) {
block := &compactionIDBlock{
segments: &datapb.IDRange{Begin: 100, End: 103},
next: 103,
end: 104,
}
_, err := block.take()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = block.take()
if err == nil {
t.Fatal("expected metadata ID exhaustion error")
}
segmentIDRange := block.segmentIDRange()
if segmentIDRange.GetBegin() != 100 || segmentIDRange.GetEnd() != 103 {
t.Fatalf("segment ID range = [%d, %d), want [100, 103)", segmentIDRange.GetBegin(), segmentIDRange.GetEnd())
}
})
}
func TestCreateCompactionIDBlockRejectsTooLargeBatch(t *testing.T) {
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "10")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
mockAlloc := allocator.NewMockAllocator(t)
_, err := createCompactionIDBlock(mockAlloc, math.MaxUint32/10+1, 1)
if err == nil {
t.Fatal("expected too-large allocation error")
}
if !strings.Contains(err.Error(), "compaction too large to allocate IDs in a single batch") {
t.Fatalf("error = %q, want too-large allocation message", err.Error())
}
}
func TestCreateCompactionIDBlockUsesIDExpansionFactor(t *testing.T) {
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "2")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
mockAlloc := allocator.NewMockAllocator(t)
mockAlloc.EXPECT().AllocN(int64(7)).Return(int64(100), int64(107), nil).Once()
block, err := createCompactionIDBlock(mockAlloc, 3, 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
segmentIDRange := block.segmentIDRange()
if segmentIDRange.GetBegin() != 100 || segmentIDRange.GetEnd() != 106 {
t.Fatalf("segment ID range = [%d, %d), want [100, 106)", segmentIDRange.GetBegin(), segmentIDRange.GetEnd())
}
}
func TestCompactionViewsExposeTotalSizeAndCollectionTTL(t *testing.T) {
ttl := 3 * time.Hour
segments := []*SegmentView{{ID: 1, Size: 10}, {ID: 2, Size: 2.5}}
tests := []struct {
name string
view CompactionView
wantTTL time.Duration
}{
{name: "single", view: &MixSegmentView{segments: segments, collectionTTL: ttl}, wantTTL: ttl},
{name: "clustering", view: &ClusteringSegmentsView{segments: segments, collectionTTL: ttl}, wantTTL: ttl},
{name: "force merge", view: &ForceMergeSegmentView{segments: segments, collectionTTL: ttl}, wantTTL: ttl},
{name: "level zero", view: &LevelZeroCompactionView{l0Segments: segments}},
{name: "bump schema version", view: &BumpSchemaVersionView{segments: segments}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := test.view.GetTotalSize(); got != 12.5 {
t.Fatalf("GetTotalSize() = %v, want 12.5", got)
}
if got := test.view.GetCollectionTTL(); got != test.wantTTL {
t.Fatalf("GetCollectionTTL() = %v, want %v", got, test.wantTTL)
}
})
}
}
// testCompactionPolicy is a minimal CompactionPolicy stub for handleTicker tests.
type testCompactionPolicy struct {
enabled bool
triggerResult map[CompactionTriggerType][]CompactionView
triggerErr error
policyName string
}
func (p *testCompactionPolicy) Enable() bool { return p.enabled }
func (p *testCompactionPolicy) Trigger(_ context.Context) (map[CompactionTriggerType][]CompactionView, error) {
return p.triggerResult, p.triggerErr
}
func (p *testCompactionPolicy) Name() string { return p.policyName }
// stubDispatchableView is a CompactionView stub for handleTicker tests that need
// a view to reach the dispatch / isFull branch.
type stubDispatchableView struct{}
func (stubDispatchableView) GetGroupLabel() *CompactionGroupLabel { return &CompactionGroupLabel{} }
func (stubDispatchableView) GetSegmentsView() []*SegmentView { return nil }
func (stubDispatchableView) Append(_ ...*SegmentView) {}
func (stubDispatchableView) String() string { return "stubDispatchableView" }
// Trigger returns (nil, "") so that notify()'s `if outView != nil` short-circuits
// before invoking the real Submit*ViewToScheduler path. Tests using this stub care
// only about the dispatch decision (isFull check), not the downstream submit.
func (stubDispatchableView) Trigger() (CompactionView, string) { return nil, "" }
func (stubDispatchableView) ForceTrigger() (CompactionView, string) { return nil, "" }
func (stubDispatchableView) ForceTriggerAll() ([]CompactionView, string) { return nil, "" }
func (stubDispatchableView) GetTriggerID() int64 { return 0 }
func (stubDispatchableView) GetTotalSize() float64 { return 0 }
func (stubDispatchableView) GetCollectionTTL() time.Duration { return 0 }
type CompactionTriggerManagerSuite struct {
suite.Suite
mockAlloc *allocator.MockAllocator
handler Handler
inspector *MockCompactionInspector
testLabel *CompactionGroupLabel
meta *meta
versionManager *MockVersionManager
triggerManager *CompactionTriggerManager
}
func (s *CompactionTriggerManagerSuite) SetupTest() {
s.mockAlloc = allocator.NewMockAllocator(s.T())
s.handler = NewNMockHandler(s.T())
s.inspector = NewMockCompactionInspector(s.T())
s.testLabel = &CompactionGroupLabel{
CollectionID: 1,
PartitionID: 10,
Channel: "ch-1",
}
segments := genSegmentsForMeta(s.testLabel)
s.meta = &meta{
segments: NewSegmentsInfo(),
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
for id, segment := range segments {
s.meta.segments.SetSegment(id, segment)
}
s.meta.collections.Insert(s.testLabel.CollectionID, &collectionInfo{
ID: s.testLabel.CollectionID,
Schema: &schemapb.CollectionSchema{},
})
versionManager := NewMockVersionManager(s.T())
versionManager.EXPECT().GetMinimalSessionVer().Return(semver.MustParse("2.7.0")).Maybe()
s.versionManager = versionManager
s.triggerManager = NewCompactionTriggerManager(s.mockAlloc, s.handler, s.inspector, s.meta, s.versionManager)
}
func (s *CompactionTriggerManagerSuite) TestNotifyByViewIDLE() {
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, mock.Anything).Return(&collectionInfo{}, nil)
s.triggerManager.handler = handler
collSegs := s.meta.GetCompactableSegmentGroupByCollection()
segments, found := collSegs[1]
s.Require().True(found)
seg1, found := lo.Find(segments, func(info *SegmentInfo) bool {
return info.ID == int64(100) && info.GetLevel() == datapb.SegmentLevel_L0
})
s.Require().True(found)
// Prepare only 1 l0 segment that doesn't meet the Trigger minimum condition
// but ViewIDLE Trigger will still forceTrigger the plan
latestL0Segments := GetViewsByInfo(seg1)
expectedSegID := seg1.ID
s.Require().Equal(1, len(latestL0Segments))
levelZeroViews := s.triggerManager.l0Policy.groupL0ViewsByPartChan(1, latestL0Segments, 10000)
s.Require().Equal(1, len(levelZeroViews))
cView, ok := levelZeroViews[0].(*LevelZeroCompactionView)
s.True(ok)
s.NotNil(cView)
mlog.Info(context.TODO(), "view", mlog.Any("cView", cView))
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(1, nil)
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(19530, task.GetTriggerID())
// s.True(signal.isGlobal)
// s.False(signal.isForce)
s.EqualValues(30000, task.GetPos().GetTimestamp())
s.Equal(s.testLabel.CollectionID, task.GetCollectionID())
s.Equal(s.testLabel.PartitionID, task.GetPartitionID())
s.Equal(s.testLabel.Channel, task.GetChannel())
s.Equal(datapb.CompactionType_Level0DeleteCompaction, task.GetType())
expectedSegs := []int64{expectedSegID}
s.ElementsMatch(expectedSegs, task.GetInputSegments())
return nil
}).Return(nil).Once()
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(19530, nil).Maybe()
s.triggerManager.notify(context.Background(), TriggerTypeLevelZeroViewIDLE, levelZeroViews)
}
func (s *CompactionTriggerManagerSuite) TestNotifyByViewChange() {
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, mock.Anything).Return(&collectionInfo{}, nil)
s.triggerManager.handler = handler
collSegs := s.meta.GetCompactableSegmentGroupByCollection()
segments, found := collSegs[1]
s.Require().True(found)
levelZeroSegments := lo.Filter(segments, func(info *SegmentInfo, _ int) bool {
return info.GetLevel() == datapb.SegmentLevel_L0
})
latestL0Segments := GetViewsByInfo(levelZeroSegments...)
s.Require().NotEmpty(latestL0Segments)
levelZeroViews := s.triggerManager.l0Policy.groupL0ViewsByPartChan(1, latestL0Segments, 10000)
s.Require().Equal(1, len(levelZeroViews))
cView, ok := levelZeroViews[0].(*LevelZeroCompactionView)
s.True(ok)
s.NotNil(cView)
mlog.Info(context.TODO(), "view", mlog.Any("cView", cView))
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(1, nil)
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(19530, task.GetTriggerID())
s.EqualValues(30000, task.GetPos().GetTimestamp())
s.Equal(s.testLabel.CollectionID, task.GetCollectionID())
s.Equal(s.testLabel.PartitionID, task.GetPartitionID())
s.Equal(s.testLabel.Channel, task.GetChannel())
s.Equal(datapb.CompactionType_Level0DeleteCompaction, task.GetType())
expectedSegs := []int64{100, 101, 102}
s.ElementsMatch(expectedSegs, task.GetInputSegments())
return nil
}).Return(nil).Once()
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(19530, nil).Maybe()
s.triggerManager.notify(context.Background(), TriggerTypeLevelZeroViewChange, levelZeroViews)
}
func (s *CompactionTriggerManagerSuite) TestManualTriggerSkipExternal() {
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, int64(1)).Return(&collectionInfo{
ID: 1,
Schema: &schemapb.CollectionSchema{
ExternalSource: "s3://external",
Fields: []*schemapb.FieldSchema{
{
FieldID: 1,
Name: "external_pk",
DataType: schemapb.DataType_Int64,
ExternalField: "pk_col",
},
},
},
}, nil)
s.triggerManager.handler = handler
_, err := s.triggerManager.ManualTrigger(context.Background(), 1, true, false, 0)
s.Error(err)
s.Contains(err.Error(), "external collection")
}
func (s *CompactionTriggerManagerSuite) TestGetExpectedSegmentSize() {
var (
collectionID = int64(1000)
fieldID = int64(2000)
indexID = int64(3000)
)
paramtable.Get().Save(paramtable.Get().DataCoordCfg.SegmentMaxSize.Key, strconv.Itoa(100))
defer paramtable.Get().Reset(paramtable.Get().DataCoordCfg.SegmentMaxSize.Key)
paramtable.Get().Save(paramtable.Get().DataCoordCfg.DiskSegmentMaxSize.Key, strconv.Itoa(200))
defer paramtable.Get().Reset(paramtable.Get().DataCoordCfg.DiskSegmentMaxSize.Key)
s.triggerManager.meta = &meta{
indexMeta: &indexMeta{
indexes: map[UniqueID]map[UniqueID]*model.Index{
collectionID: {
indexID + 1: &model.Index{
CollectionID: collectionID,
FieldID: fieldID + 1,
IndexID: indexID + 1,
IndexName: "",
IsDeleted: false,
CreateTime: 0,
TypeParams: nil,
IndexParams: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "DISKANN"},
},
IsAutoIndex: false,
UserIndexParams: nil,
},
indexID + 2: &model.Index{
CollectionID: collectionID,
FieldID: fieldID + 2,
IndexID: indexID + 2,
IndexName: "",
IsDeleted: false,
CreateTime: 0,
TypeParams: nil,
IndexParams: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "DISKANN"},
},
IsAutoIndex: false,
UserIndexParams: nil,
},
},
},
},
}
s.Run("all DISKANN", func() {
collection := &collectionInfo{
ID: collectionID,
Schema: &schemapb.CollectionSchema{
Name: "coll1",
Description: "",
Fields: []*schemapb.FieldSchema{
{FieldID: fieldID, Name: "field0", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: fieldID + 1, Name: "field1", DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
{FieldID: fieldID + 2, Name: "field2", DataType: schemapb.DataType_Float16Vector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
},
EnableDynamicField: false,
Properties: nil,
},
}
s.Equal(int64(200*1024*1024), getExpectedSegmentSize(s.triggerManager.meta, collection.ID, collection.Schema))
})
s.Run("HNSW & DISKANN", func() {
s.triggerManager.meta = &meta{
indexMeta: &indexMeta{
indexes: map[UniqueID]map[UniqueID]*model.Index{
collectionID: {
indexID + 1: &model.Index{
CollectionID: collectionID,
FieldID: fieldID + 1,
IndexID: indexID + 1,
IndexName: "",
IsDeleted: false,
CreateTime: 0,
TypeParams: nil,
IndexParams: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "HNSW"},
},
IsAutoIndex: false,
UserIndexParams: nil,
},
indexID + 2: &model.Index{
CollectionID: collectionID,
FieldID: fieldID + 2,
IndexID: indexID + 2,
IndexName: "",
IsDeleted: false,
CreateTime: 0,
TypeParams: nil,
IndexParams: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "DISKANN"},
},
IsAutoIndex: false,
UserIndexParams: nil,
},
},
},
},
}
collection := &collectionInfo{
ID: collectionID,
Schema: &schemapb.CollectionSchema{
Name: "coll1",
Description: "",
Fields: []*schemapb.FieldSchema{
{FieldID: fieldID, Name: "field0", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: fieldID + 1, Name: "field1", DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
{FieldID: fieldID + 2, Name: "field2", DataType: schemapb.DataType_Float16Vector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
},
EnableDynamicField: false,
Properties: nil,
},
}
s.Equal(int64(100*1024*1024), getExpectedSegmentSize(s.triggerManager.meta, collection.ID, collection.Schema))
})
s.Run("some vector has no index", func() {
s.triggerManager.meta = &meta{
indexMeta: &indexMeta{
indexes: map[UniqueID]map[UniqueID]*model.Index{
collectionID: {
indexID + 1: &model.Index{
CollectionID: collectionID,
FieldID: fieldID + 1,
IndexID: indexID + 1,
IndexName: "",
IsDeleted: false,
CreateTime: 0,
TypeParams: nil,
IndexParams: []*commonpb.KeyValuePair{
{Key: common.IndexTypeKey, Value: "HNSW"},
},
IsAutoIndex: false,
UserIndexParams: nil,
},
},
},
},
}
collection := &collectionInfo{
ID: collectionID,
Schema: &schemapb.CollectionSchema{
Name: "coll1",
Description: "",
Fields: []*schemapb.FieldSchema{
{FieldID: fieldID, Name: "field0", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: fieldID + 1, Name: "field1", DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
{FieldID: fieldID + 2, Name: "field2", DataType: schemapb.DataType_Float16Vector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "8"}}},
},
EnableDynamicField: false,
Properties: nil,
},
}
s.Equal(int64(100*1024*1024), getExpectedSegmentSize(s.triggerManager.meta, collection.ID, collection.Schema))
})
}
func (s *CompactionTriggerManagerSuite) TestManualTriggerL0Compaction() {
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, mock.Anything).Return(&collectionInfo{}, nil)
s.triggerManager.handler = handler
collSegs := s.meta.GetCompactableSegmentGroupByCollection()
segments, found := collSegs[1]
s.Require().True(found)
levelZeroSegments := lo.Filter(segments, func(info *SegmentInfo, _ int) bool {
return info.GetLevel() == datapb.SegmentLevel_L0
})
s.Require().NotEmpty(levelZeroSegments)
// Mock allocator for trigger ID
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(int64(12345), nil)
s.mockAlloc.EXPECT().AllocID(mock.Anything).Return(int64(19530), nil).Maybe()
// Mock inspector to expect compaction enqueue
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(19530, task.GetTriggerID())
s.Equal(s.testLabel.CollectionID, task.GetCollectionID())
s.Equal(s.testLabel.PartitionID, task.GetPartitionID())
s.Equal(s.testLabel.Channel, task.GetChannel())
s.Equal(datapb.CompactionType_Level0DeleteCompaction, task.GetType())
expectedSegs := []int64{100, 101, 102}
s.ElementsMatch(expectedSegs, task.GetInputSegments())
return nil
}).Return(nil).Once()
// Test L0 manual trigger
triggerID, err := s.triggerManager.ManualTrigger(context.Background(), s.testLabel.CollectionID, false, true, 0)
s.NoError(err)
s.Equal(int64(12345), triggerID)
}
func (s *CompactionTriggerManagerSuite) TestManualTriggerInvalidParams() {
// Test with both clustering and L0 compaction false
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, mock.Anything).Return(&collectionInfo{}, nil)
s.triggerManager.handler = handler
triggerID, err := s.triggerManager.ManualTrigger(context.Background(), s.testLabel.CollectionID, false, false, 0)
s.NoError(err)
s.Equal(int64(0), triggerID)
}
func (s *CompactionTriggerManagerSuite) TestSubmitSingleViewToScheduler() {
makeMixView := func(segments []*SegmentView) *MixSegmentView {
return &MixSegmentView{
label: s.testLabel,
segments: segments,
collectionTTL: 100,
triggerID: 1001,
}
}
s.Run("mix compaction allocates estimated result segments", func() {
s.SetupTest()
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "1")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
pt.Save(pt.DataCoordCfg.SegmentMaxSize.Key, "100")
defer pt.Reset(pt.DataCoordCfg.SegmentMaxSize.Key)
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 100, Name: "vec", DataType: schemapb.DataType_FloatVector},
},
}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
const (
startID = int64(500)
endID = int64(504)
planID = int64(503)
)
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(planID, task.GetPlanID())
s.EqualValues(1001, task.GetTriggerID())
s.Equal(datapb.CompactionType_MixCompaction, task.GetType())
s.Equal(s.testLabel.CollectionID, task.GetCollectionID())
s.Equal(s.testLabel.PartitionID, task.GetPartitionID())
s.Equal(s.testLabel.Channel, task.GetChannel())
s.Equal(collectionSchema, task.GetSchema())
s.ElementsMatch([]int64{200, 201}, task.GetInputSegments())
s.EqualValues(300, task.GetTotalRows())
s.Equal(&datapb.IDRange{Begin: startID, End: planID}, task.GetPreAllocatedSegmentIDs())
s.Equal(task.GetStartTime(), task.GetLastStateStartTime())
return nil
}).Return(nil).Once()
view := makeMixView([]*SegmentView{
{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024},
{ID: 201, label: s.testLabel, NumOfRows: 200, Size: 150 * 1024 * 1024},
})
s.triggerManager.SubmitSingleViewToScheduler(context.Background(), view, TriggerTypeSingle)
})
s.Run("sort compaction allocates estimated result segments", func() {
s.SetupTest()
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "1")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
pt.Save(pt.DataCoordCfg.SegmentMaxSize.Key, "100")
defer pt.Reset(pt.DataCoordCfg.SegmentMaxSize.Key)
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 100, Name: "vec", DataType: schemapb.DataType_FloatVector},
},
}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
const (
startID = int64(600)
endID = int64(604)
planID = int64(603)
)
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.Equal(datapb.CompactionType_SortCompaction, task.GetType())
s.Equal(&datapb.IDRange{Begin: startID, End: planID}, task.GetPreAllocatedSegmentIDs())
return nil
}).Return(nil).Once()
view := makeMixView([]*SegmentView{
{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 300 * 1024 * 1024},
})
s.triggerManager.SubmitSingleViewToScheduler(context.Background(), view, TriggerTypeSort)
})
s.Run("storage version upgrade keeps size based estimation", func() {
s.SetupTest()
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "1")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
pt.Save(pt.DataCoordCfg.SegmentMaxSize.Key, "100")
defer pt.Reset(pt.DataCoordCfg.SegmentMaxSize.Key)
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
{FieldID: 100, Name: "vec", DataType: schemapb.DataType_FloatVector},
},
}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
const (
startID = int64(700)
endID = int64(704)
planID = int64(703)
)
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.Equal(datapb.CompactionType_MixCompaction, task.GetType())
s.Equal(&datapb.IDRange{Begin: startID, End: planID}, task.GetPreAllocatedSegmentIDs())
return nil
}).Return(nil).Once()
view := makeMixView([]*SegmentView{
{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 300 * 1024 * 1024},
})
s.triggerManager.SubmitSingleViewToScheduler(context.Background(), view, TriggerTypeStorageVersionUpgrade)
})
}
func (s *CompactionTriggerManagerSuite) TestSubmitClusteringViewToScheduler() {
s.SetupTest()
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "2")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
pt.Save(pt.DataCoordCfg.SegmentMaxSize.Key, "100")
defer pt.Reset(pt.DataCoordCfg.SegmentMaxSize.Key)
pt.Save(pt.DataCoordCfg.ClusteringCompactionPreferSegmentSizeRatio.Key, "1")
defer pt.Reset(pt.DataCoordCfg.ClusteringCompactionPreferSegmentSizeRatio.Key)
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
clusteringKey := &schemapb.FieldSchema{FieldID: 2, Name: "cluster_key", DataType: schemapb.DataType_Int64, IsClusteringKey: true}
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
clusteringKey,
{FieldID: 100, Name: "vec", DataType: schemapb.DataType_FloatVector},
},
}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
const (
startID = int64(800)
segmentIDEnd = int64(806)
planID = int64(806)
analyzeTaskID = int64(807)
endID = int64(808)
)
s.mockAlloc.EXPECT().AllocN(int64(8)).Return(startID, endID, nil).Once()
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.Equal(datapb.CompactionType_ClusteringCompaction, task.GetType())
s.EqualValues(planID, task.GetPlanID())
s.EqualValues(analyzeTaskID, task.GetAnalyzeTaskID())
s.Equal(&datapb.IDRange{Begin: startID, End: segmentIDEnd}, task.GetPreAllocatedSegmentIDs())
s.Equal(task.GetStartTime(), task.GetLastStateStartTime())
return nil
}).Return(nil).Once()
view := &ClusteringSegmentsView{
label: s.testLabel,
segments: []*SegmentView{{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024}, {ID: 201, label: s.testLabel, NumOfRows: 200, Size: 150 * 1024 * 1024}},
clusteringKeyField: clusteringKey,
collectionTTL: 100,
triggerID: 1001,
}
s.triggerManager.SubmitClusteringViewToScheduler(context.Background(), view)
}
func (s *CompactionTriggerManagerSuite) TestSubmitForceMergeViewToScheduler() {
s.SetupTest()
pt := paramtable.Get()
pt.Save(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "1")
defer pt.Reset(pt.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
const (
startID = int64(500)
segmentIDEnd = int64(502)
planID = int64(502)
endID = int64(503)
)
s.mockAlloc.EXPECT().AllocN(int64(3)).Return(startID, endID, nil).Once()
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(planID, task.GetPlanID())
s.Equal(datapb.CompactionType_MixCompaction, task.GetType())
s.Equal(&datapb.IDRange{Begin: startID, End: segmentIDEnd}, task.GetPreAllocatedSegmentIDs())
return nil
}).Return(nil).Once()
view := &ForceMergeSegmentView{
label: s.testLabel,
segments: []*SegmentView{{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024}},
triggerID: 1001,
targetSegmentSize: 100 * 1024 * 1024,
targetSegmentCount: 999,
}
s.triggerManager.SubmitForceMergeViewToScheduler(context.Background(), view)
}
func (s *CompactionTriggerManagerSuite) TestSubmitViewToSchedulerDefensiveReturns() {
makeCollectionSchema := func(external bool) *schemapb.CollectionSchema {
field := &schemapb.FieldSchema{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true}
if external {
field.ExternalField = "pk_col"
}
return &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{field},
}
}
makeMixView := func() *MixSegmentView {
return &MixSegmentView{
label: s.testLabel,
segments: []*SegmentView{{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024}},
collectionTTL: 100,
triggerID: 1001,
}
}
makeClusteringView := func(clusteringKey *schemapb.FieldSchema) *ClusteringSegmentsView {
return &ClusteringSegmentsView{
label: s.testLabel,
segments: []*SegmentView{{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024}},
clusteringKeyField: clusteringKey,
collectionTTL: 100,
triggerID: 1001,
}
}
makeForceMergeView := func() *ForceMergeSegmentView {
return &ForceMergeSegmentView{
label: s.testLabel,
segments: []*SegmentView{{ID: 200, label: s.testLabel, NumOfRows: 100, Size: 150 * 1024 * 1024}},
triggerID: 1001,
targetSegmentSize: 100 * 1024 * 1024,
targetSegmentCount: 999,
}
}
s.Run("single GetCollection error returns before allocation", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(nil, errors.New("get collection error")).Once()
s.triggerManager.handler = handler
s.triggerManager.SubmitSingleViewToScheduler(context.Background(), makeMixView(), TriggerTypeSingle)
})
s.Run("single AllocN error returns before enqueue", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: makeCollectionSchema(false)}, nil).Once()
s.triggerManager.handler = handler
s.mockAlloc.EXPECT().AllocN(mock.Anything).Return(int64(0), int64(0), errors.New("alloc error")).Once()
s.triggerManager.SubmitSingleViewToScheduler(context.Background(), makeMixView(), TriggerTypeSingle)
})
s.Run("clustering GetCollection error returns before allocation", func() {
s.SetupTest()
clusteringKey := &schemapb.FieldSchema{FieldID: 2, Name: "cluster_key", DataType: schemapb.DataType_Int64, IsClusteringKey: true}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(nil, errors.New("get collection error")).Once()
s.triggerManager.handler = handler
s.triggerManager.SubmitClusteringViewToScheduler(context.Background(), makeClusteringView(clusteringKey))
})
s.Run("clustering AllocN error returns before enqueue", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
clusteringKey := &schemapb.FieldSchema{FieldID: 2, Name: "cluster_key", DataType: schemapb.DataType_Int64, IsClusteringKey: true}
collectionSchema := makeCollectionSchema(false)
collectionSchema.Fields = append(collectionSchema.Fields, clusteringKey)
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
s.mockAlloc.EXPECT().AllocN(mock.Anything).Return(int64(0), int64(0), errors.New("alloc error")).Once()
s.triggerManager.SubmitClusteringViewToScheduler(context.Background(), makeClusteringView(clusteringKey))
})
s.Run("force merge GetCollection error returns before allocation", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(nil, errors.New("get collection error")).Once()
s.triggerManager.handler = handler
s.triggerManager.SubmitForceMergeViewToScheduler(context.Background(), makeForceMergeView())
})
s.Run("force merge nil collection returns before allocation", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).Return(nil, nil).Once()
s.triggerManager.handler = handler
s.triggerManager.SubmitForceMergeViewToScheduler(context.Background(), makeForceMergeView())
})
s.Run("force merge external collection returns before allocation", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: makeCollectionSchema(true)}, nil).Once()
s.triggerManager.handler = handler
s.triggerManager.SubmitForceMergeViewToScheduler(context.Background(), makeForceMergeView())
})
s.Run("force merge AllocN error returns before enqueue", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: makeCollectionSchema(false)}, nil).Once()
s.triggerManager.handler = handler
s.mockAlloc.EXPECT().AllocN(mock.Anything).Return(int64(0), int64(0), errors.New("alloc error")).Once()
s.triggerManager.SubmitForceMergeViewToScheduler(context.Background(), makeForceMergeView())
})
}
func (s *CompactionTriggerManagerSuite) TestSubmitBumpSchemaVersionViewToScheduler() {
Params.Save(Params.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key, "1")
defer Params.Reset(Params.DataCoordCfg.CompactionPreAllocateIDExpansionFactor.Key)
Params.Save(Params.DataCoordCfg.SegmentMaxSize.Key, "100")
defer Params.Reset(Params.DataCoordCfg.SegmentMaxSize.Key)
Params.Save(Params.DataCoordCfg.DiskSegmentMaxSize.Key, "100")
defer Params.Reset(Params.DataCoordCfg.DiskSegmentMaxSize.Key)
collectionSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true},
},
Functions: []*schemapb.FunctionSchema{
{Name: "bm25_fn", Type: schemapb.FunctionType_BM25, OutputFieldIds: []int64{100}},
},
}
makeBumpSchemaVersionView := func(triggerID int64) *BumpSchemaVersionView {
segView := &SegmentView{
ID: 200,
label: s.testLabel,
NumOfRows: 1000,
Size: 300 * 1024 * 1024,
}
return &BumpSchemaVersionView{
label: s.testLabel,
segments: []*SegmentView{segView},
triggerID: triggerID,
schema: collectionSchema,
}
}
s.Run("AllocN fails", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: collectionSchema}, nil).Once()
s.triggerManager.handler = handler
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(int64(0), int64(0), errors.New("alloc error")).Once()
view := makeBumpSchemaVersionView(111)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("GetCollection fails", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(nil, errors.New("get collection error")).Once()
s.triggerManager.handler = handler
view := makeBumpSchemaVersionView(111)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("collection is nil", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(nil, nil).Once()
s.triggerManager.handler = handler
view := makeBumpSchemaVersionView(111)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("collection is external", func() {
s.SetupTest()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{
ID: s.testLabel.CollectionID,
// IsExternal() checks for fields with ExternalField set, not Schema.ExternalSource.
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 1, Name: "ext_field", ExternalField: "s3://bucket/external"},
},
},
}, nil).Once()
s.triggerManager.handler = handler
view := makeBumpSchemaVersionView(111)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("view is not BumpSchemaVersionView", func() {
s.SetupTest()
// Use LevelZeroCompactionView which is NOT a *BumpSchemaVersionView.
nonBumpSchemaVersionView := &LevelZeroCompactionView{
label: s.testLabel,
l0Segments: []*SegmentView{},
}
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), nonBumpSchemaVersionView)
})
s.Run("enqueueCompaction fails", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
const (
startID = int64(504)
endID = int64(508)
triggerID = int64(999)
)
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{
ID: s.testLabel.CollectionID,
Schema: collectionSchema,
}, nil).Once()
s.triggerManager.handler = handler
s.inspector.EXPECT().enqueueCompaction(mock.Anything).Return(errors.New("enqueue error")).Once()
view := makeBumpSchemaVersionView(triggerID)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("success", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
const (
startID = int64(600)
endID = int64(604)
planID = int64(603)
triggerID = int64(1001)
)
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{
ID: s.testLabel.CollectionID,
Schema: collectionSchema,
Properties: map[string]string{common.CollectionTTLConfigKey: "3600"},
}, nil).Once()
s.triggerManager.handler = handler
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.EqualValues(planID, task.GetPlanID())
s.EqualValues(triggerID, task.GetTriggerID())
s.Equal(datapb.CompactionType_BumpSchemaVersionCompaction, task.GetType())
s.Equal(s.testLabel.CollectionID, task.GetCollectionID())
s.Equal(s.testLabel.PartitionID, task.GetPartitionID())
s.Equal(s.testLabel.Channel, task.GetChannel())
s.Equal(collectionSchema, task.GetSchema())
s.ElementsMatch([]int64{200}, task.GetInputSegments())
s.Empty(task.GetResultSegments())
s.NotZero(task.GetStartTime())
s.NotZero(task.GetLastStateStartTime())
s.NotNil(task.GetPreAllocatedSegmentIDs())
s.EqualValues(startID, task.GetPreAllocatedSegmentIDs().GetBegin())
s.EqualValues(planID, task.GetPreAllocatedSegmentIDs().GetEnd())
s.EqualValues(time.Hour.Nanoseconds(), task.GetCollectionTtl())
return nil
}).Once()
view := makeBumpSchemaVersionView(triggerID)
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
s.Run("policy to submit keeps frozen schema", func() {
s.SetupTest()
s.meta.indexMeta = &indexMeta{indexes: make(map[UniqueID]map[UniqueID]*model.Index)}
const (
startID = int64(601)
endID = int64(605)
planID = int64(604)
triggerID = int64(1002)
)
frozenSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Version: 2,
Fields: []*schemapb.FieldSchema{{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true}},
}
liveSchema := &schemapb.CollectionSchema{
Name: "test_coll",
Version: 3,
Fields: []*schemapb.FieldSchema{{FieldID: 1, Name: "pk", DataType: schemapb.DataType_Int64, IsPrimaryKey: true}},
}
s.mockAlloc.EXPECT().AllocN(int64(4)).Return(startID, endID, nil).Once()
handler := NewNMockHandler(s.T())
handler.EXPECT().GetCollection(mock.Anything, s.testLabel.CollectionID).
Return(&collectionInfo{ID: s.testLabel.CollectionID, Schema: liveSchema}, nil).Once()
s.triggerManager.handler = handler
s.inspector.EXPECT().enqueueCompaction(mock.Anything).
RunAndReturn(func(task *datapb.CompactionTask) error {
s.Equal(datapb.CompactionType_BumpSchemaVersionCompaction, task.GetType())
s.EqualValues(2, task.GetSchema().GetVersion())
s.Equal(frozenSchema, task.GetSchema())
s.NotNil(task.GetPreAllocatedSegmentIDs())
s.EqualValues(startID, task.GetPreAllocatedSegmentIDs().GetBegin())
s.EqualValues(planID, task.GetPreAllocatedSegmentIDs().GetEnd())
return nil
}).Once()
view := makeBumpSchemaVersionView(triggerID)
view.schema = frozenSchema
s.triggerManager.SubmitBumpSchemaVersionViewToScheduler(context.Background(), view)
})
}
func (s *CompactionTriggerManagerSuite) TestHandleTicker() {
s.Run("policy not found for ticker type", func() {
s.SetupTest()
// TickerType 99 is not registered in policies map
s.triggerManager.handleTicker(context.Background(), TickerType(99))
// Should return without panic
})
s.Run("policy disabled", func() {
s.SetupTest()
mockPolicy := &testCompactionPolicy{enabled: false, policyName: "test-disabled"}
s.triggerManager.policies[BumpSchemaVersionTicker] = mockPolicy
s.triggerManager.handleTicker(context.Background(), BumpSchemaVersionTicker)
// Returns early — no inspector or trigger calls
})
s.Run("inspector full skips Trigger dispatch", func() {
// When isFull() returns true, Trigger() is not called and no schema-bump task is submitted.
s.SetupTest()
mockPolicy := &testCompactionPolicy{
enabled: true,
policyName: "test-full",
// Views in Trigger() result are NOT dispatched because isFull() returns true.
triggerResult: map[CompactionTriggerType][]CompactionView{
TriggerTypeBumpSchemaVersion: {stubDispatchableView{}},
},
}
s.triggerManager.policies[BumpSchemaVersionTicker] = mockPolicy
s.inspector.EXPECT().isFull().Return(true).Once()
s.triggerManager.handleTicker(context.Background(), BumpSchemaVersionTicker)
})
s.Run("policy trigger error returns before isFull check", func() {
s.SetupTest()
mockPolicy := &testCompactionPolicy{
enabled: true,
policyName: "test-err",
triggerErr: errors.New("trigger error"),
}
s.triggerManager.policies[BumpSchemaVersionTicker] = mockPolicy
// isFull() IS called now (step 2, before Trigger). Trigger() errors → no notify().
s.inspector.EXPECT().isFull().Return(false).Once()
s.triggerManager.handleTicker(context.Background(), BumpSchemaVersionTicker)
})
s.Run("policy trigger returns no events skips isFull check", func() {
s.SetupTest()
mockPolicy := &testCompactionPolicy{
enabled: true,
policyName: "test-empty",
// Nil result — Trigger() returns nothing, notify() is never called.
triggerResult: nil,
}
s.triggerManager.policies[BumpSchemaVersionTicker] = mockPolicy
// isFull() IS called now (step 2 gate before Trigger). Trigger returns nil → no notify.
s.inspector.EXPECT().isFull().Return(false).Once()
s.triggerManager.handleTicker(context.Background(), BumpSchemaVersionTicker)
})
s.Run("policy trigger returns events dispatched when inspector not full", func() {
s.SetupTest()
mockPolicy := &testCompactionPolicy{
enabled: true,
policyName: "test-events",
triggerResult: map[CompactionTriggerType][]CompactionView{
TriggerTypeBumpSchemaVersion: {stubDispatchableView{}},
},
}
s.triggerManager.policies[BumpSchemaVersionTicker] = mockPolicy
s.inspector.EXPECT().isFull().Return(false).Once()
// stubDispatchableView.Trigger() returns nil, so notify() short-circuits
// before reaching SubmitBumpSchemaVersionViewToScheduler. We only care here that
// isFull was consulted and the dispatch path was entered.
s.triggerManager.handleTicker(context.Background(), BumpSchemaVersionTicker)
})
}