Files
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

663 lines
20 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 proxy
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
"github.com/milvus-io/milvus/internal/mocks"
"github.com/milvus-io/milvus/pkg/v3/util/conc"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
)
// Tests for ResolveCollectionAlias in MetaCache and resolveCollectionAlias helper.
func TestResolveCollectionAlias_WildcardAndEmptySkippedByInterceptor(t *testing.T) {
// The interceptor guard (objectName != util.AnyWord && objectName != "") ensures
// that "*" and "" never reach resolveCollectionAlias. This test verifies those
// values are correctly guarded at the interceptor level by checking that the
// cache's DescribeAlias RPC is never called for these sentinel values.
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
// Wildcard "*" should not trigger resolution
for _, sentinel := range []string{"*", ""} {
// These sentinel values should be caught by the interceptor guard
// (objectName != util.AnyWord && objectName != "") before calling
// resolveCollectionAlias. Verify the guard logic directly.
shouldSkip := sentinel == "*" || sentinel == ""
assert.True(t, shouldSkip, "sentinel %q should be skipped by interceptor guard", sentinel)
}
// Normal name should resolve via RPC
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{ErrorCode: commonpb.ErrorCode_Success},
Collection: "real_col",
}, nil)
result, err := resolveCollectionAlias(ctx, "default", "some_alias")
assert.NoError(t, err)
assert.Equal(t, "real_col", result)
mockCoord.AssertCalled(t, "DescribeAlias", mock.Anything, mock.Anything)
}
func TestResolveCollectionAlias_CachedCollection(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {
"test_collection": {
collID: 1,
schema: &schemaInfo{},
},
},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
result, err := cache.ResolveCollectionAlias(ctx, "default", "test_collection")
assert.NoError(t, err)
assert.Equal(t, "test_collection", result)
mockCoord.AssertNotCalled(t, "DescribeAlias")
}
func TestResolveCollectionAlias_ValidAlias(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
mockCoord.On("DescribeAlias", mock.Anything, mock.MatchedBy(func(req *milvuspb.DescribeAliasRequest) bool {
return req.DbName == "default" && req.Alias == "my_alias"
})).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_Success,
},
Collection: "actual_collection",
}, nil)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
result, err := cache.ResolveCollectionAlias(ctx, "default", "my_alias")
assert.NoError(t, err)
assert.Equal(t, "actual_collection", result)
}
func TestResolveCollectionAlias_NotAnAlias(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
mockCoord.On("DescribeAlias", mock.Anything, mock.MatchedBy(func(req *milvuspb.DescribeAliasRequest) bool {
return req.DbName == "default" && req.Alias == "not_an_alias"
})).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_CollectionNotExists,
Reason: "alias not found",
},
}, nil)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
// Should return original name when alias not found (not error)
result, err := cache.ResolveCollectionAlias(ctx, "default", "not_an_alias")
assert.NoError(t, err)
assert.Equal(t, "not_an_alias", result)
}
func TestResolveCollectionAlias_RPCError(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).
Return(nil, assert.AnError)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
result, err := cache.ResolveCollectionAlias(ctx, "default", "some_name")
assert.Error(t, err)
assert.Equal(t, "", result)
}
func TestResolveCollectionAlias_InternalServerError(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Internal server error should be propagated, not swallowed
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_UnexpectedError,
Reason: "internal error",
},
}, nil)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
result, err := cache.ResolveCollectionAlias(ctx, "default", "some_name")
assert.Error(t, err)
assert.Equal(t, "", result)
}
func TestResolveCollectionAlias_EmptyCollectionInResponse(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Status is success but collection field is empty
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_Success,
},
Collection: "",
}, nil)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
result, err := cache.ResolveCollectionAlias(ctx, "default", "some_alias")
assert.NoError(t, err)
assert.Equal(t, "some_alias", result)
}
func TestResolveCollectionAlias_NilGlobalMetaCache(t *testing.T) {
ctx := context.Background()
oldCache := globalMetaCache
globalMetaCache = nil
defer func() { globalMetaCache = oldCache }()
result, err := resolveCollectionAlias(ctx, "default", "test")
assert.Error(t, err)
assert.Equal(t, "test", result)
}
func TestResolveCollectionAlias_AliasCacheHit(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"my_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
// Should return cached alias without RPC call
result, err := cache.ResolveCollectionAlias(ctx, "default", "my_alias")
assert.NoError(t, err)
assert.Equal(t, "real_collection", result)
mockCoord.AssertNotCalled(t, "DescribeAlias")
}
func TestResolveCollectionAlias_NegativeCacheHit(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"regular_name": {collectionName: "", cachedAt: time.Now()},
},
},
}
// Negative cache: not an alias, return as-is without RPC
result, err := cache.ResolveCollectionAlias(ctx, "default", "regular_name")
assert.NoError(t, err)
assert.Equal(t, "regular_name", result)
mockCoord.AssertNotCalled(t, "DescribeAlias")
}
func TestResolveCollectionAlias_PopulatesPositiveCache(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_Success,
},
Collection: "real_collection",
}, nil).Once()
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
// First call triggers RPC
result, err := cache.ResolveCollectionAlias(ctx, "default", "my_alias")
assert.NoError(t, err)
assert.Equal(t, "real_collection", result)
// Second call should hit cache, no additional RPC
result, err = cache.ResolveCollectionAlias(ctx, "default", "my_alias")
assert.NoError(t, err)
assert.Equal(t, "real_collection", result)
// DescribeAlias should have been called exactly once
mockCoord.AssertNumberOfCalls(t, "DescribeAlias", 1)
}
func TestResolveCollectionAlias_PopulatesNegativeCache(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
mockCoord.On("DescribeAlias", mock.Anything, mock.Anything).Return(&milvuspb.DescribeAliasResponse{
Status: &commonpb.Status{
ErrorCode: commonpb.ErrorCode_CollectionNotExists,
Reason: "alias not found",
},
}, nil).Once()
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{},
}
// First call triggers RPC
result, err := cache.ResolveCollectionAlias(ctx, "default", "not_alias")
assert.NoError(t, err)
assert.Equal(t, "not_alias", result)
// Second call should hit negative cache
result, err = cache.ResolveCollectionAlias(ctx, "default", "not_alias")
assert.NoError(t, err)
assert.Equal(t, "not_alias", result)
mockCoord.AssertNumberOfCalls(t, "DescribeAlias", 1)
}
func TestRemoveAlias_InvalidatesCache(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"my_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
// Remove the alias
cache.RemoveAlias(ctx, "default", "my_alias")
// Verify the alias was removed from cache
_, ok := cache.getAlias("default", "my_alias")
assert.False(t, ok)
}
func TestRemoveCollectionByID_CleansUpAliases(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {
"my_collection": {
collID: 100,
schema: &schemaInfo{},
},
},
},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"alias1": {collectionName: "my_collection", cachedAt: time.Now()},
"alias2": {collectionName: "my_collection", cachedAt: time.Now()},
"alias3": {collectionName: "other_collection", cachedAt: time.Now()},
},
},
collectionCacheVersion: make(map[UniqueID]uint64),
sfGlobal: conc.Singleflight[*collectionInfo]{},
collLevelPartitionCache: NewVersionCache[string, *partitionInfos](),
partitionCache: NewVersionCache[string, *partitionInfo](),
}
// Remove collection by ID
cache.RemoveCollectionsByID(ctx, 100, 0, true)
// Aliases pointing to my_collection should be removed
_, ok := cache.getAlias("default", "alias1")
assert.False(t, ok)
_, ok = cache.getAlias("default", "alias2")
assert.False(t, ok)
// Alias pointing to other_collection should remain
entry, ok := cache.getAlias("default", "alias3")
assert.True(t, ok)
assert.Equal(t, "other_collection", entry.collectionName)
}
func TestRemoveCollection_CleansUpAliasesWhenNotCached(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Collection is NOT in collInfo cache, but aliases pointing to it exist
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"default": {},
},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"alias1": {collectionName: "uncached_collection", cachedAt: time.Now()},
"alias2": {collectionName: "uncached_collection", cachedAt: time.Now()},
"alias3": {collectionName: "other_collection", cachedAt: time.Now()},
},
},
collectionCacheVersion: make(map[UniqueID]uint64),
sfGlobal: conc.Singleflight[*collectionInfo]{},
}
// RemoveCollection for an uncached collection
cache.RemoveCollection(ctx, "default", "uncached_collection", 0)
// Aliases pointing to uncached_collection should be removed
_, ok := cache.getAlias("default", "alias1")
assert.False(t, ok)
_, ok = cache.getAlias("default", "alias2")
assert.False(t, ok)
// Alias pointing to other_collection should remain
entry, ok := cache.getAlias("default", "alias3")
assert.True(t, ok)
assert.Equal(t, "other_collection", entry.collectionName)
}
func TestRemoveDatabase_CleansUpAliases(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{
"mydb": {},
},
aliasInfo: map[string]map[string]*aliasEntry{
"mydb": {
"alias1": {collectionName: "coll1", cachedAt: time.Now()},
},
},
dbInfo: map[string]*databaseInfo{
"mydb": {},
},
collLevelPartitionCache: NewVersionCache[string, *partitionInfos](),
partitionCache: NewVersionCache[string, *partitionInfo](),
}
cache.RemoveDatabase(ctx, "mydb")
// Alias cache for the database should be gone
_, ok := cache.getAlias("mydb", "alias1")
assert.False(t, ok)
}
func TestCreateAliasTask_ResolvesCollectionAlias(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Set up cache: "existing_alias" -> "real_collection"
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"existing_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
paramtable.Init()
task := &CreateAliasTask{
Condition: NewTaskCondition(ctx),
CreateAliasRequest: &milvuspb.CreateAliasRequest{
Base: &commonpb.MsgBase{},
DbName: "default",
CollectionName: "existing_alias",
Alias: "new_alias",
},
ctx: ctx,
mixCoord: mockCoord,
}
err := task.PreExecute(ctx)
assert.NoError(t, err)
// CollectionName should always be resolved from alias to real collection
assert.Equal(t, "real_collection", task.CollectionName)
}
func TestAlterAliasTask_ResolvesCollectionAlias(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Set up cache: "existing_alias" -> "real_collection"
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"existing_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
paramtable.Init()
task := &AlterAliasTask{
Condition: NewTaskCondition(ctx),
AlterAliasRequest: &milvuspb.AlterAliasRequest{
Base: &commonpb.MsgBase{},
DbName: "default",
CollectionName: "existing_alias",
Alias: "some_alias",
},
ctx: ctx,
mixCoord: mockCoord,
}
err := task.PreExecute(ctx)
assert.NoError(t, err)
// CollectionName should always be resolved from alias to real collection
assert.Equal(t, "real_collection", task.CollectionName)
}
func TestCreateAliasTask_ResolvesEvenWhenRBACFlagDisabled(t *testing.T) {
// Alias resolution in CreateAlias/AlterAlias is unconditional (for correctness),
// independent of the RBAC feature flag.
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"existing_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
paramtable.Init()
paramtable.Get().Save(Params.ProxyCfg.ResolveAliasForPrivilege.Key, "false")
defer paramtable.Get().Reset(Params.ProxyCfg.ResolveAliasForPrivilege.Key)
task := &CreateAliasTask{
Condition: NewTaskCondition(ctx),
CreateAliasRequest: &milvuspb.CreateAliasRequest{
Base: &commonpb.MsgBase{},
DbName: "default",
CollectionName: "existing_alias",
Alias: "new_alias",
},
ctx: ctx,
mixCoord: mockCoord,
}
err := task.PreExecute(ctx)
assert.NoError(t, err)
// CollectionName should be resolved even when RBAC flag is disabled
assert.Equal(t, "real_collection", task.CollectionName)
}
func TestListAliasesTask_ResolvesCollectionAlias(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
// Set up cache: "existing_alias" -> "real_collection"
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{
"default": {
"existing_alias": {collectionName: "real_collection", cachedAt: time.Now()},
},
},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
paramtable.Init()
task := &ListAliasesTask{
Condition: NewTaskCondition(ctx),
ListAliasesRequest: &milvuspb.ListAliasesRequest{
Base: &commonpb.MsgBase{},
DbName: "default",
CollectionName: "existing_alias",
},
ctx: ctx,
mixCoord: mockCoord,
}
err := task.PreExecute(ctx)
assert.NoError(t, err)
// CollectionName should be resolved from alias to real collection
assert.Equal(t, "real_collection", task.CollectionName)
}
func TestListAliasesTask_NoResolveWhenCollectionNameEmpty(t *testing.T) {
ctx := context.Background()
mockCoord := mocks.NewMockMixCoordClient(t)
cache := &MetaCache{
mixCoord: mockCoord,
collInfo: map[string]map[string]*collectionInfo{"default": {}},
aliasInfo: map[string]map[string]*aliasEntry{},
}
oldCache := globalMetaCache
globalMetaCache = cache
defer func() { globalMetaCache = oldCache }()
paramtable.Init()
task := &ListAliasesTask{
Condition: NewTaskCondition(ctx),
ListAliasesRequest: &milvuspb.ListAliasesRequest{
Base: &commonpb.MsgBase{},
DbName: "default",
},
ctx: ctx,
mixCoord: mockCoord,
}
err := task.PreExecute(ctx)
assert.NoError(t, err)
// CollectionName should remain empty
assert.Equal(t, "", task.CollectionName)
mockCoord.AssertNotCalled(t, "DescribeAlias")
}