// 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 datacoord import ( "testing" "time" "github.com/samber/lo" "github.com/stretchr/testify/assert" "stathat.com/c/consistent" memkv "github.com/milvus-io/milvus/internal/kv/mem" ) func TestBufferChannelAssignPolicy(t *testing.T) { kv := memkv.NewMemoryKV() channels := []RWChannel{getChannel("chan1", 1)} store := &ChannelStore{ store: kv, channelsInfo: map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{}}, bufferID: {bufferID, channels}, }, } updates := BufferChannelAssignPolicy(store, 1).Collect() assert.NotNil(t, updates) assert.Equal(t, 2, len(updates)) assert.ElementsMatch(t, NewChannelOpSet( NewAddOp(1, channels...), NewDeleteOp(bufferID, channels...), ).Collect(), updates) } func getChannel(name string, collID int64) *channelMeta { return &channelMeta{Name: name, CollectionID: collID} } func getChannels(ch2Coll map[string]int64) []RWChannel { return lo.MapToSlice(ch2Coll, func(name string, coll int64) RWChannel { return &channelMeta{Name: name, CollectionID: coll} }) } func TestConsistentHashRegisterPolicy(t *testing.T) { t.Run("first register", func(t *testing.T) { kv := memkv.NewMemoryKV() ch2Coll := map[string]int64{ "chan1": 1, "chan2": 2, } channels := getChannels(ch2Coll) store := &ChannelStore{ store: kv, channelsInfo: map[int64]*NodeChannelInfo{ bufferID: {bufferID, channels}, 1: {1, []RWChannel{}}, }, } hashring := consistent.New() policy := ConsistentHashRegisterPolicy(hashring) up, _ := policy(store, 1) updates := up.Collect() assert.NotNil(t, updates) assert.Equal(t, 2, len(updates)) assert.EqualValues(t, &ChannelOp{Type: Delete, NodeID: bufferID, Channels: channels}, updates[0]) assert.EqualValues(t, &ChannelOp{Type: Add, NodeID: 1, Channels: channels}, updates[1]) }) t.Run("rebalance after register", func(t *testing.T) { kv := memkv.NewMemoryKV() ch2Coll := map[string]int64{ "chan1": 1, "chan2": 2, } channels := getChannels(ch2Coll) store := &ChannelStore{ store: kv, channelsInfo: map[int64]*NodeChannelInfo{1: {1, channels}, 2: {2, []RWChannel{}}}, } hashring := consistent.New() hashring.Add(formatNodeID(1)) policy := ConsistentHashRegisterPolicy(hashring) _, up := policy(store, 2) updates := up.Collect() assert.NotNil(t, updates) assert.Equal(t, 1, len(updates)) // No Delete operation will be generated assert.Equal(t, 1, len(updates[0].GetChannelNames())) channel := updates[0].GetChannelNames()[0] // Not stable whether to balance chan-1 or chan-2 if channel == "chan-1" { assert.EqualValues(t, &ChannelOp{Type: Add, NodeID: 1, Channels: []RWChannel{channels[0]}}, updates[0]) } if channel == "chan-2" { assert.EqualValues(t, &ChannelOp{Type: Add, NodeID: 1, Channels: []RWChannel{channels[1]}}, updates[0]) } }) } func TestAverageAssignPolicy(t *testing.T) { type args struct { store ROChannelStore channels []RWChannel } tests := []struct { name string args args want *ChannelOpSet }{ { "test assign empty cluster", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{}, }, []RWChannel{getChannel("chan1", 1)}, }, NewChannelOpSet(NewAddOp(bufferID, getChannel("chan1", 1))), }, { "test watch same channel", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, }, }, []RWChannel{getChannel("chan1", 1)}, }, NewChannelOpSet(), }, { "test normal assign", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, 2: {2, []RWChannel{getChannel("chan3", 1)}}, }, }, []RWChannel{getChannel("chan4", 1)}, }, NewChannelOpSet(NewAddOp(2, getChannel("chan4", 1))), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AverageAssignPolicy(tt.args.store, tt.args.channels) assert.EqualValues(t, tt.want.Collect(), got.Collect()) }) } } func TestConsistentHashChannelAssignPolicy(t *testing.T) { type args struct { hashring *consistent.Consistent store ROChannelStore channels []RWChannel } tests := []struct { name string args args want *ChannelOpSet }{ { "test assign empty cluster", args{ consistent.New(), &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{}, }, []RWChannel{getChannel("chan1", 1)}, }, NewChannelOpSet(NewAddOp(bufferID, getChannel("chan1", 1))), }, { "test watch same channel", args{ consistent.New(), &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, }, }, []RWChannel{getChannel("chan1", 1)}, }, NewChannelOpSet(), }, { "test normal watch", args{ consistent.New(), &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{1: {1, nil}, 2: {2, nil}, 3: {3, nil}}, }, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1)}, }, NewChannelOpSet( NewAddOp(2, getChannel("chan1", 1)), NewAddOp(1, getChannel("chan2", 1)), NewAddOp(3, getChannel("chan3", 1)), ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policy := ConsistentHashChannelAssignPolicy(tt.args.hashring) got := policy(tt.args.store, tt.args.channels).Collect() want := tt.want.Collect() assert.Equal(t, len(want), len(got)) for _, op := range want { assert.Contains(t, got, op) } }) } } func TestAvgAssignUnregisteredChannels(t *testing.T) { type args struct { store ROChannelStore nodeID int64 } tests := []struct { name string args args want *ChannelOpSet }{ { "test deregister the last node", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, }, }, 1, }, NewChannelOpSet( NewDeleteOp(1, getChannel("chan1", 1)), NewAddOp(bufferID, getChannel("chan1", 1)), ), }, { "test rebalance channels after deregister", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, 2: {2, []RWChannel{getChannel("chan2", 1)}}, 3: {3, []RWChannel{}}, }, }, 2, }, NewChannelOpSet( NewDeleteOp(2, getChannel("chan2", 1)), NewAddOp(3, getChannel("chan2", 1)), ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AvgAssignUnregisteredChannels(tt.args.store, tt.args.nodeID) assert.EqualValues(t, tt.want.Collect(), got.Collect()) }) } } func TestConsistentHashDeregisterPolicy(t *testing.T) { type args struct { hashring *consistent.Consistent store ROChannelStore nodeID int64 } tests := []struct { name string args args want *ChannelOpSet }{ { "test deregister the last node", args{ consistent.New(), &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, }, }, 1, }, NewChannelOpSet( NewDeleteOp(1, getChannel("chan1", 1)), NewAddOp(bufferID, getChannel("chan1", 1)), ), }, { "rebalance after deregister", args{ consistent.New(), &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan2", 1)}}, 2: {2, []RWChannel{getChannel("chan1", 1)}}, 3: {3, []RWChannel{getChannel("chan3", 1)}}, }, }, 2, }, NewChannelOpSet( NewDeleteOp(2, getChannel("chan1", 1)), NewAddOp(1, getChannel("chan1", 1)), ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policy := ConsistentHashDeregisterPolicy(tt.args.hashring) got := policy(tt.args.store, tt.args.nodeID) assert.EqualValues(t, tt.want.Collect(), got.Collect()) }) } } func TestRoundRobinReassignPolicy(t *testing.T) { type args struct { store ROChannelStore reassigns []*NodeChannelInfo } tests := []struct { name string args args want *ChannelOpSet }{ { "test only one node", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, }, }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1)}}}, }, NewChannelOpSet(), }, { "test normal reassigning", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, 2: {2, []RWChannel{}}, }, }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}}, }, NewChannelOpSet( NewDeleteOp(1, getChannel("chan1", 1), getChannel("chan2", 1)), NewAddOp(2, getChannel("chan1", 1), getChannel("chan2", 1)), ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := RoundRobinReassignPolicy(tt.args.store, tt.args.reassigns) assert.EqualValues(t, tt.want.Collect(), got.Collect()) }) } } func TestBgCheckForChannelBalance(t *testing.T) { type args struct { channels []*NodeChannelInfo timestamp time.Time } tests := []struct { name string args args want []*NodeChannelInfo wantErr error }{ { "test even distribution", args{ []*NodeChannelInfo{ {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, {2, []RWChannel{getChannel("chan1", 2), getChannel("chan2", 2)}}, {3, []RWChannel{getChannel("chan1", 3), getChannel("chan2", 3)}}, }, time.Now(), }, // there should be no reallocate []*NodeChannelInfo{}, nil, }, { "test uneven with conservative effect", args{ []*NodeChannelInfo{ {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, {2, []RWChannel{}}, }, time.Now(), }, // as we deem that the node having only one channel more than average as even, so there's no reallocation // for this test case []*NodeChannelInfo{}, nil, }, { "test uneven with zero", args{ []*NodeChannelInfo{ {1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), }}, {2, []RWChannel{}}, }, time.Now(), }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1)}}}, nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policy := BgBalanceCheck got, err := policy(tt.args.channels, tt.args.timestamp) assert.Equal(t, tt.wantErr, err) assert.EqualValues(t, tt.want, got) }) } } func TestAvgReassignPolicy(t *testing.T) { type args struct { store ROChannelStore reassigns []*NodeChannelInfo } tests := []struct { name string args args want *ChannelOpSet }{ { "test_only_one_node", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, }, }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1)}}}, }, // as there's no available nodes except the input node, there's no reassign plan generated NewChannelOpSet(), }, { "test_zero_avg", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1)}}, 2: {2, []RWChannel{}}, 3: {2, []RWChannel{}}, 4: {2, []RWChannel{}}, }, }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1)}}}, }, // as we use ceil to calculate the wanted average number, there should be one reassign // though the average num less than 1 NewChannelOpSet( NewDeleteOp(1, getChannel("chan1", 1)), NewAddOp(2, getChannel("chan1", 1)), ), }, { "test_normal_reassigning_for_one_available_nodes", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}, 2: {2, []RWChannel{}}, }, }, []*NodeChannelInfo{{1, []RWChannel{getChannel("chan1", 1), getChannel("chan2", 1)}}}, }, NewChannelOpSet( NewDeleteOp(1, getChannel("chan1", 1), getChannel("chan2", 1)), NewAddOp(2, getChannel("chan1", 1), getChannel("chan2", 1)), ), }, { "test_normal_reassigning_for_multiple_available_nodes", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), }}, 2: {2, []RWChannel{}}, 3: {3, []RWChannel{}}, 4: {4, []RWChannel{}}, }, }, []*NodeChannelInfo{{1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), }}}, }, NewChannelOpSet( NewDeleteOp(1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), }...), NewAddOp(2, getChannel("chan1", 1)), NewAddOp(3, getChannel("chan2", 1)), NewAddOp(4, getChannel("chan3", 1)), ), }, { "test_reassigning_for_extreme_case", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), getChannel("chan5", 1), getChannel("chan6", 1), getChannel("chan7", 1), getChannel("chan8", 1), getChannel("chan9", 1), getChannel("chan10", 1), getChannel("chan11", 1), getChannel("chan12", 1), }}, 2: {2, []RWChannel{ getChannel("chan13", 1), getChannel("chan14", 1), }}, 3: {3, []RWChannel{getChannel("chan15", 1)}}, 4: {4, []RWChannel{}}, }, }, []*NodeChannelInfo{{1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), getChannel("chan5", 1), getChannel("chan6", 1), getChannel("chan7", 1), getChannel("chan8", 1), getChannel("chan9", 1), getChannel("chan10", 1), getChannel("chan11", 1), getChannel("chan12", 1), }}}, }, NewChannelOpSet( NewDeleteOp(1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), getChannel("chan5", 1), getChannel("chan6", 1), getChannel("chan7", 1), getChannel("chan8", 1), getChannel("chan9", 1), getChannel("chan10", 1), getChannel("chan11", 1), getChannel("chan12", 1), }...), NewAddOp(4, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), getChannel("chan5", 1), }...), NewAddOp(3, []RWChannel{ getChannel("chan6", 1), getChannel("chan7", 1), getChannel("chan8", 1), getChannel("chan9", 1), }...), NewAddOp(2, []RWChannel{ getChannel("chan10", 1), getChannel("chan11", 1), getChannel("chan12", 1), }...), ), }, } for _, tt := range tests { if tt.name == "test_reassigning_for_extreme_case" || tt.name == "test_normal_reassigning_for_multiple_available_nodes" { continue } t.Run(tt.name, func(t *testing.T) { got := AverageReassignPolicy(tt.args.store, tt.args.reassigns) assert.ElementsMatch(t, tt.want.Collect(), got.Collect()) }) } } func TestAvgBalanceChannelPolicy(t *testing.T) { type args struct { store ROChannelStore } tests := []struct { name string args args want *ChannelOpSet }{ { "test_only_one_node", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: { 1, []RWChannel{ getChannel("chan1", 1), getChannel("chan2", 1), getChannel("chan3", 1), getChannel("chan4", 1), }, }, 2: {2, []RWChannel{}}, }, }, }, NewChannelOpSet(NewAddOp(1, getChannel("chan1", 1))), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AvgBalanceChannelPolicy(tt.args.store, time.Now()) assert.EqualValues(t, tt.want.Collect(), got.Collect()) }) } } func TestAvgAssignRegisterPolicy(t *testing.T) { type args struct { store ROChannelStore nodeID int64 } tests := []struct { name string args args bufferedUpdates *ChannelOpSet balanceUpdates *ChannelOpSet }{ { "test empty", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {NodeID: 1, Channels: make([]RWChannel, 0)}, }, }, 1, }, NewChannelOpSet(), NewChannelOpSet(), }, { "test with buffer channel", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ bufferID: {bufferID, []RWChannel{getChannel("ch1", 1)}}, 1: {NodeID: 1, Channels: []RWChannel{}}, }, }, 1, }, NewChannelOpSet( NewDeleteOp(bufferID, getChannel("ch1", 1)), NewAddOp(1, getChannel("ch1", 1)), ), NewChannelOpSet(), }, { "test with avg assign", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("ch1", 1), getChannel("ch2", 1)}}, 3: {3, []RWChannel{}}, }, }, 3, }, NewChannelOpSet(), NewChannelOpSet(NewAddOp(1, getChannel("ch1", 1))), }, { "test with avg equals to zero", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("ch1", 1)}}, 2: {2, []RWChannel{getChannel("ch3", 1)}}, 3: {3, []RWChannel{}}, }, }, 3, }, NewChannelOpSet(), NewChannelOpSet(), }, { "test node with empty channel", args{ &ChannelStore{ memkv.NewMemoryKV(), map[int64]*NodeChannelInfo{ 1: {1, []RWChannel{getChannel("ch1", 1), getChannel("ch2", 1), getChannel("ch3", 1)}}, 2: {2, []RWChannel{}}, 3: {3, []RWChannel{}}, }, }, 3, }, NewChannelOpSet(), NewChannelOpSet(NewAddOp(1, getChannel("ch1", 1))), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bufferedUpdates, balanceUpdates := AvgAssignRegisterPolicy(tt.args.store, tt.args.nodeID) assert.EqualValues(t, tt.bufferedUpdates.Collect(), bufferedUpdates.Collect()) assert.EqualValues(t, tt.balanceUpdates.Collect(), balanceUpdates.Collect()) }) } }