Skip to content

Commit eee98fd

Browse files
authored
fix: [2.5] Fix collections with duplicate names can be created (#40147)
This PR introduces two restrictions: 1. Before dropping a collection, all aliases associated with that collection must be dropped. 2. When creating a collection, if the collection name duplicates any alias, the collection creation will fail. issue: #40142 pr: #40143 --------- Signed-off-by: bigsheeper <[email protected]>
1 parent d60511a commit eee98fd

8 files changed

+111
-20
lines changed

internal/rootcoord/create_collection_task.go

+9
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,14 @@ func (t *createCollectionTask) Execute(ctx context.Context) error {
598598
UpdateTimestamp: ts,
599599
}
600600

601+
// Check if the collection name duplicates an alias.
602+
_, err = t.core.meta.DescribeAlias(ctx, t.Req.GetDbName(), t.Req.GetCollectionName(), typeutil.MaxTimestamp)
603+
if err == nil {
604+
err2 := fmt.Errorf("collection name [%s] conflicts with an existing alias, please choose a unique name", t.Req.GetCollectionName())
605+
log.Ctx(ctx).Warn("create collection failed", zap.String("database", t.Req.GetDbName()), zap.Error(err2))
606+
return err2
607+
}
608+
601609
// We cannot check the idempotency inside meta table when adding collection, since we'll execute duplicate steps
602610
// if add collection successfully due to idempotency check. Some steps may be risky to be duplicate executed if they
603611
// are not promised idempotent.
@@ -613,6 +621,7 @@ func (t *createCollectionTask) Execute(ctx context.Context) error {
613621
log.Ctx(ctx).Warn("add duplicate collection", zap.String("collection", t.Req.GetCollectionName()), zap.Uint64("ts", ts))
614622
return nil
615623
}
624+
log.Ctx(ctx).Info("check collection existence", zap.String("collection", t.Req.GetCollectionName()), zap.Error(err))
616625

617626
// TODO: The create collection is not idempotent for other component, such as wal.
618627
// we need to make the create collection operation must success after some persistent operation, refactor it in future.

internal/rootcoord/create_collection_task_test.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"math"
2222
"strconv"
23+
"strings"
2324
"testing"
2425
"time"
2526

@@ -903,6 +904,8 @@ func Test_createCollectionTask_Execute(t *testing.T) {
903904
mock.Anything,
904905
mock.Anything,
905906
).Return(coll, nil)
907+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
908+
Return("", merr.WrapErrAliasNotFound("", ""))
906909

907910
core := newTestCore(withMeta(meta), withTtSynchronizer(ticker))
908911

@@ -950,6 +953,8 @@ func Test_createCollectionTask_Execute(t *testing.T) {
950953
mock.Anything,
951954
mock.Anything,
952955
).Return(coll, nil)
956+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
957+
Return("", merr.WrapErrAliasNotFound("", ""))
953958

954959
core := newTestCore(withMeta(meta), withTtSynchronizer(ticker))
955960

@@ -972,10 +977,11 @@ func Test_createCollectionTask_Execute(t *testing.T) {
972977
ticker := newTickerWithMockFailStream()
973978
shardNum := 2
974979
pchans := ticker.getDmlChannelNames(shardNum)
975-
meta := newMockMetaTable()
976-
meta.GetCollectionByNameFunc = func(ctx context.Context, collectionName string, ts Timestamp) (*model.Collection, error) {
977-
return nil, errors.New("error mock GetCollectionByName")
978-
}
980+
meta := mockrootcoord.NewIMetaTable(t)
981+
meta.EXPECT().GetCollectionByName(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
982+
Return(nil, errors.New("error mock GetCollectionByName"))
983+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
984+
Return("", merr.WrapErrAliasNotFound("", ""))
979985
core := newTestCore(withTtSynchronizer(ticker), withMeta(meta))
980986
schema := &schemapb.CollectionSchema{Name: "", Fields: []*schemapb.FieldSchema{{}}}
981987
task := &createCollectionTask{
@@ -996,6 +1002,40 @@ func Test_createCollectionTask_Execute(t *testing.T) {
9961002
assert.Error(t, err)
9971003
})
9981004

1005+
t.Run("collection name duplicates an alias", func(t *testing.T) {
1006+
defer cleanTestEnv()
1007+
1008+
collectionName := funcutil.GenRandomStr()
1009+
ticker := newRocksMqTtSynchronizer()
1010+
field1 := funcutil.GenRandomStr()
1011+
schema := &schemapb.CollectionSchema{Name: collectionName, Fields: []*schemapb.FieldSchema{{Name: field1}}}
1012+
1013+
meta := mockrootcoord.NewIMetaTable(t)
1014+
// mock collection name duplicates an alias
1015+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1016+
Return(collectionName, nil)
1017+
1018+
core := newTestCore(withMeta(meta), withTtSynchronizer(ticker))
1019+
task := &createCollectionTask{
1020+
baseTask: newBaseTask(context.Background(), core),
1021+
Req: &milvuspb.CreateCollectionRequest{
1022+
Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_CreateCollection},
1023+
DbName: "mock-db",
1024+
CollectionName: collectionName,
1025+
Properties: []*commonpb.KeyValuePair{
1026+
{
1027+
Key: common.ConsistencyLevel,
1028+
Value: "1",
1029+
},
1030+
},
1031+
},
1032+
schema: schema,
1033+
}
1034+
err := task.Execute(context.Background())
1035+
assert.Error(t, err)
1036+
assert.True(t, strings.Contains(err.Error(), "conflicts with an existing alias"))
1037+
})
1038+
9991039
t.Run("normal case", func(t *testing.T) {
10001040
defer cleanTestEnv()
10011041

@@ -1023,6 +1063,8 @@ func Test_createCollectionTask_Execute(t *testing.T) {
10231063
mock.Anything,
10241064
mock.Anything,
10251065
).Return(nil)
1066+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1067+
Return("", merr.WrapErrAliasNotFound("", ""))
10261068

10271069
dc := newMockDataCoord()
10281070
dc.GetComponentStatesFunc = func(ctx context.Context) (*milvuspb.ComponentStates, error) {
@@ -1107,6 +1149,8 @@ func Test_createCollectionTask_Execute(t *testing.T) {
11071149
mock.Anything,
11081150
mock.Anything,
11091151
).Return(errors.New("error mock ChangeCollectionState"))
1152+
meta.EXPECT().DescribeAlias(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1153+
Return("", merr.WrapErrAliasNotFound("", ""))
11101154

11111155
removeCollectionCalled := false
11121156
removeCollectionChan := make(chan struct{}, 1)

internal/rootcoord/drop_collection_task.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,20 @@ func (t *dropCollectionTask) Execute(ctx context.Context) error {
6363
log.Ctx(ctx).Warn("drop non-existent collection", zap.String("collection", t.Req.GetCollectionName()), zap.String("database", t.Req.GetDbName()))
6464
return nil
6565
}
66-
6766
if err != nil {
6867
return err
6968
}
7069

7170
// meta cache of all aliases should also be cleaned.
7271
aliases := t.core.meta.ListAliasesByID(ctx, collMeta.CollectionID)
7372

73+
// Check if all aliases have been dropped.
74+
if len(aliases) > 0 {
75+
err = fmt.Errorf("unable to drop the collection [%s] because it has associated aliases %v, please remove all aliases before dropping the collection", t.Req.GetCollectionName(), aliases)
76+
log.Ctx(ctx).Warn("drop collection failed", zap.String("database", t.Req.GetDbName()), zap.Error(err))
77+
return err
78+
}
79+
7480
ts := t.GetTs()
7581
return executeDropCollectionTaskSteps(ctx,
7682
t.core, collMeta, t.Req.GetDbName(), aliases,

internal/rootcoord/drop_collection_task_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package rootcoord
1818

1919
import (
2020
"context"
21+
"strings"
2122
"testing"
2223
"time"
2324

@@ -181,6 +182,40 @@ func Test_dropCollectionTask_Execute(t *testing.T) {
181182
assert.Error(t, err)
182183
})
183184

185+
t.Run("aliases have not been dropped", func(t *testing.T) {
186+
defer cleanTestEnv()
187+
188+
collectionName := funcutil.GenRandomStr()
189+
shardNum := 2
190+
191+
ticker := newRocksMqTtSynchronizer()
192+
pchans := ticker.getDmlChannelNames(shardNum)
193+
ticker.addDmlChannels(pchans...)
194+
195+
coll := &model.Collection{Name: collectionName, ShardsNum: int32(shardNum), PhysicalChannelNames: pchans}
196+
197+
meta := mockrootcoord.NewIMetaTable(t)
198+
meta.EXPECT().GetCollectionByName(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
199+
Return(coll.Clone(), nil)
200+
meta.EXPECT().ListAliasesByID(mock.Anything, mock.Anything).
201+
Return([]string{"mock-alias-0", "mock-alias-1"})
202+
203+
core := newTestCore(
204+
withMeta(meta),
205+
withTtSynchronizer(ticker))
206+
207+
task := &dropCollectionTask{
208+
baseTask: newBaseTask(context.Background(), core),
209+
Req: &milvuspb.DropCollectionRequest{
210+
Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_DropCollection},
211+
CollectionName: collectionName,
212+
},
213+
}
214+
err := task.Execute(context.Background())
215+
assert.Error(t, err)
216+
assert.True(t, strings.Contains(err.Error(), "please remove all aliases"))
217+
})
218+
184219
t.Run("normal case, redo", func(t *testing.T) {
185220
defer cleanTestEnv()
186221

tests/python_client/milvus_client/test_milvus_client_alias.py

+4
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def test_milvus_client_create_same_alias_diff_collections(self):
179179
f"[alias={alias}]"}
180180
self.create_alias(client, collection_name_1, alias,
181181
check_task=CheckTasks.err_res, check_items=error)
182+
self.drop_alias(client, alias)
182183
self.drop_collection(client, collection_name)
183184

184185
@pytest.mark.tags(CaseLabel.L1)
@@ -349,6 +350,7 @@ def test_milvus_client_alter_non_exists_alias(self):
349350
error = {ct.err_code: 1600, ct.err_msg: f"alias not found[database=default][alias={another_alias}]"}
350351
self.alter_alias(client, collection_name, another_alias,
351352
check_task=CheckTasks.err_res, check_items=error)
353+
self.drop_alias(client, alias)
352354
self.drop_collection(client, collection_name)
353355

354356

@@ -477,4 +479,6 @@ def test_milvus_client_alter_alias_default(self):
477479
# 4. assert collection is equal to alias according to partitions
478480
partition_name_list_alias = self.list_partitions(client, another_alias)[0]
479481
assert partition_name_list == partition_name_list_alias
482+
self.drop_alias(client, alias)
483+
self.drop_alias(client, another_alias)
480484
self.drop_collection(client, collection_name)

tests/python_client/milvus_client/test_milvus_client_search_iterator.py

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def test_milvus_client_search_iterator_alias_different_col(self, search_params):
122122
self.release_collection(client, collection_name)
123123
self.drop_collection(client, collection_name)
124124
self.release_collection(client, collection_name_new)
125+
self.drop_alias(client, alias)
125126
self.drop_collection(client, collection_name_new)
126127

127128

tests/python_client/testcases/test_alias.py

+6-15
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def test_alias_called_by_utility_drop_collection(self):
222222
self.utility_wrap.drop_collection(alias_name,
223223
check_task=CheckTasks.err_res,
224224
check_items=error)
225+
self.utility_wrap.drop_alias(alias_name)
225226
self.utility_wrap.drop_collection(c_name)
226227
assert not self.utility_wrap.has_collection(c_name)[0]
227228

@@ -447,20 +448,17 @@ def test_alias_reuse_alias_name_from_dropped_collection(self):
447448
assert len(res) == 1
448449

449450
# dropping collection that has an alias shall drop the alias as well
451+
self.utility_wrap.drop_alias(alias_name)
450452
collection_w.drop()
451453
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
452454
check_task=CheckTasks.check_collection_property,
453455
check_items={exp_name: c_name, exp_schema: default_schema})
454456
res2 = self.utility_wrap.list_aliases(c_name)[0]
455457
assert len(res2) == 0
456458
# the same alias name can be reused for another collection
457-
error = {ct.err_code: 999,
458-
ct.err_msg: f"{alias_name} is alias to another collection: {collection_w.name}: alias already exist"}
459-
self.utility_wrap.create_alias(c_name, alias_name,
460-
check_task=CheckTasks.err_res,
461-
check_items=error)
462-
# res2 = self.utility_wrap.list_aliases(c_name)[0]
463-
# assert len(res2) == 1
459+
self.utility_wrap.create_alias(c_name, alias_name)
460+
res2 = self.utility_wrap.list_aliases(c_name)[0]
461+
assert len(res2) == 1
464462

465463
@pytest.mark.tags(CaseLabel.L0)
466464
def test_alias_rename_collection_to_alias_name(self):
@@ -469,7 +467,7 @@ def test_alias_rename_collection_to_alias_name(self):
469467
method:
470468
1.create a collection
471469
2.create an alias for the collection
472-
3.rename the collection to the alias name no matter the collection was dropped or not
470+
3.rename the collection to the alias name
473471
expected: in step 3, rename collection to alias name failed
474472
"""
475473
self._connect()
@@ -483,10 +481,3 @@ def test_alias_rename_collection_to_alias_name(self):
483481
ct.err_msg: f"cannot rename collection to an existing alias: {alias_name}"}
484482
self.utility_wrap.rename_collection(collection_w.name, alias_name,
485483
check_task=CheckTasks.err_res, check_items=error)
486-
487-
collection_w.drop()
488-
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
489-
check_task=CheckTasks.check_collection_property,
490-
check_items={exp_name: c_name, exp_schema: default_schema})
491-
self.utility_wrap.rename_collection(collection_w.name, alias_name,
492-
check_task=CheckTasks.err_res, check_items=error)

tests/python_client/testcases/test_query.py

+1
Original file line numberDiff line numberDiff line change
@@ -3737,6 +3737,7 @@ def test_count_alias_insert_delete_drop(self):
37373737
collection_w_alias.drop(check_task=CheckTasks.err_res,
37383738
check_items={ct.err_code: 1,
37393739
ct.err_msg: "cannot drop the collection via alias"})
3740+
self.utility_wrap.drop_alias(alias)
37403741
collection_w.drop()
37413742

37423743
@pytest.mark.tags(CaseLabel.L2)

0 commit comments

Comments
 (0)