mirror of
https://gitee.com/milvus-io/milvus.git
synced 2026-01-07 19:31:51 +08:00
Add support for DataNode compaction using file resources in ref mode. SortCompation and StatsJobs will build text indexes, which may use file resources. relate: https://github.com/milvus-io/milvus/issues/43687 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> - Core invariant: file resources (analyzer binaries/metadata) are only fetched, downloaded and used when the node is configured in Ref mode (fileresource.IsRefMode via CommonCfg.QNFileResourceMode / DNFileResourceMode); Sync now carries a version and managers track per-resource versions/resource IDs so newer resource sets win and older entries are pruned (RefManager/SynchManager resource maps). - Logic removed / simplified: component-specific FileResourceMode flags and an indirection through a long-lived BinlogIO wrapper were consolidated — file-resource mode moved to CommonCfg, Sync/Download APIs became version- and context-aware, and compaction/index tasks accept a ChunkManager directly (binlog IO wrapper creation inlined). This eliminates duplicated config checks and wrapper indirection while preserving the same chunk/IO semantics. - Why no data loss or behavior regression: all file-resource code paths are gated by the configured mode (default remains "sync"); when not in ref-mode or when no resources exist, compaction and stats flows follow existing code paths unchanged. Versioned Sync + resourceID maps ensure newly synced sets replace older ones and RefManager prunes stale files; GetFileResources returns an error if requested IDs are missing (prevents silent use of wrong resources). Analyzer naming/parameter changes add analyzer_extra_info but default-callers pass "" so existing analyzers and index contents remain unchanged. - New capability: DataNode compaction and StatsJobs can now build text indexes using external file resources in Ref mode — DataCoord exposes GetFileResources and populates CompactionPlan.file_resources; SortCompaction/StatsTask download resources via fileresource.Manager, produce an analyzer_extra_info JSON (storage + resource->id map) via analyzer.BuildExtraResourceInfo, and propagate analyzer_extra_info into BuildIndexInfo so the tantivy bindings can load custom analyzers during text index creation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: aoiasd <zhicheng.yue@zilliz.com>
262 lines
7.3 KiB
Go
262 lines
7.3 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 observers
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/milvus-io/milvus-proto/go-api/v2/commonpb"
|
|
"github.com/milvus-io/milvus/internal/querycoordv2/session"
|
|
"github.com/milvus-io/milvus/pkg/v2/proto/internalpb"
|
|
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
|
|
)
|
|
|
|
type FileResourceObserverSuite struct {
|
|
suite.Suite
|
|
|
|
ctx context.Context
|
|
observer *FileResourceObserver
|
|
|
|
// Real components
|
|
nodeManager *session.NodeManager
|
|
mockCluster *session.MockCluster
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) SetupSuite() {
|
|
paramtable.Init()
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) SetupTest() {
|
|
suite.ctx = context.Background()
|
|
|
|
// Create real NodeManager and mock Cluster
|
|
suite.nodeManager = session.NewNodeManager()
|
|
suite.mockCluster = session.NewMockCluster(suite.T())
|
|
|
|
// Create FileResourceObserver
|
|
suite.observer = NewFileResourceObserver(suite.ctx, suite.nodeManager, suite.mockCluster)
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TearDownTest() {
|
|
// Assert mock expectations for cluster only
|
|
suite.mockCluster.AssertExpectations(suite.T())
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestNewFileResourceObserver() {
|
|
observer := NewFileResourceObserver(suite.ctx, suite.nodeManager, suite.mockCluster)
|
|
suite.NotNil(observer)
|
|
suite.Equal(suite.ctx, observer.ctx)
|
|
suite.Equal(suite.nodeManager, observer.nodeManager)
|
|
suite.Equal(suite.mockCluster, observer.cluster)
|
|
suite.NotNil(observer.distribution)
|
|
suite.NotNil(observer.notifyCh)
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestNotify() {
|
|
// Test notify without blocking
|
|
suite.observer.Notify()
|
|
|
|
// Verify notification was sent
|
|
select {
|
|
case <-suite.observer.notifyCh:
|
|
// Expected
|
|
case <-time.After(100 * time.Millisecond):
|
|
suite.Fail("Expected notification but got none")
|
|
}
|
|
|
|
// Test notify when channel is full (should not block)
|
|
suite.observer.Notify()
|
|
suite.observer.Notify() // This should not block even if channel is full
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestUpdateResources() {
|
|
resources := []*internalpb.FileResourceInfo{
|
|
{
|
|
Id: 1,
|
|
Name: "test.file",
|
|
Path: "/test/test.file",
|
|
},
|
|
}
|
|
version := uint64(100)
|
|
|
|
// Update resources
|
|
suite.observer.UpdateResources(resources, version)
|
|
|
|
// Verify resources and version are updated
|
|
resultResources, resultVersion := suite.observer.getResources()
|
|
suite.Equal(resources, resultResources)
|
|
suite.Equal(version, resultVersion)
|
|
|
|
// Verify notification was sent
|
|
select {
|
|
case <-suite.observer.notifyCh:
|
|
// Expected
|
|
case <-time.After(100 * time.Millisecond):
|
|
suite.Fail("Expected notification but got none")
|
|
}
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestGetResources() {
|
|
resources := []*internalpb.FileResourceInfo{
|
|
{
|
|
Id: 1,
|
|
Name: "test.file",
|
|
Path: "/test/test.file",
|
|
},
|
|
}
|
|
version := uint64(100)
|
|
|
|
// Set resources directly
|
|
suite.observer.resources = resources
|
|
suite.observer.version = version
|
|
|
|
// Get resources
|
|
resultResources, resultVersion := suite.observer.getResources()
|
|
suite.Equal(resources, resultResources)
|
|
suite.Equal(version, resultVersion)
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestSync_Success() {
|
|
// Prepare test data
|
|
resources := []*internalpb.FileResourceInfo{
|
|
{
|
|
Id: 1,
|
|
Name: "test.file",
|
|
Path: "/test/test.file",
|
|
},
|
|
}
|
|
version := uint64(100)
|
|
|
|
// Real nodeManager starts with empty node list
|
|
// Execute sync
|
|
err := suite.observer.sync(resources, version)
|
|
|
|
// Verify no error since no nodes to sync
|
|
suite.NoError(err)
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestSync_WithNodes() {
|
|
// Add some nodes to the real nodeManager
|
|
node1 := session.NewNodeInfo(session.ImmutableNodeInfo{
|
|
NodeID: 1,
|
|
Address: "localhost:19530",
|
|
Hostname: "node1",
|
|
})
|
|
node2 := session.NewNodeInfo(session.ImmutableNodeInfo{
|
|
NodeID: 2,
|
|
Address: "localhost:19531",
|
|
Hostname: "node2",
|
|
})
|
|
suite.nodeManager.Add(node1)
|
|
suite.nodeManager.Add(node2)
|
|
|
|
// Prepare test data
|
|
resources := []*internalpb.FileResourceInfo{
|
|
{
|
|
Id: 1,
|
|
Name: "test.file",
|
|
Path: "/test/test.file",
|
|
},
|
|
}
|
|
version := uint64(100)
|
|
|
|
// Mock cluster sync calls for each node
|
|
req1 := &internalpb.SyncFileResourceRequest{Resources: resources, Version: version}
|
|
req2 := &internalpb.SyncFileResourceRequest{Resources: resources, Version: version}
|
|
suite.mockCluster.EXPECT().SyncFileResource(suite.ctx, int64(1), req1).Return(&commonpb.Status{ErrorCode: commonpb.ErrorCode_Success}, nil)
|
|
suite.mockCluster.EXPECT().SyncFileResource(suite.ctx, int64(2), req2).Return(&commonpb.Status{ErrorCode: commonpb.ErrorCode_Success}, nil)
|
|
|
|
// Execute sync
|
|
err := suite.observer.sync(resources, version)
|
|
|
|
// Verify no error
|
|
suite.NoError(err)
|
|
|
|
// Verify version was updated for both nodes
|
|
suite.Equal(version, suite.observer.distribution[1])
|
|
suite.Equal(version, suite.observer.distribution[2])
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestSync_NodeSyncError() {
|
|
// Prepare test data
|
|
resources := []*internalpb.FileResourceInfo{}
|
|
version := uint64(100)
|
|
|
|
// Real nodeManager starts with empty node list
|
|
// Execute sync
|
|
err := suite.observer.sync(resources, version)
|
|
|
|
// Verify no error since no nodes to sync
|
|
suite.NoError(err)
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestStart_SyncModeEnabled() {
|
|
// Mock paramtable to enable sync mode
|
|
paramtable.Get().CommonCfg.QNFileResourceMode.SwapTempValue("sync")
|
|
|
|
// Start observer - real nodeManager starts with empty node list
|
|
suite.observer.Start()
|
|
|
|
// Wait a bit for goroutine to start
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Verify observer started (no specific expectations to check since real nodeManager is used)
|
|
// The test passes if no panic or error occurs
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestStart_SyncModeDisabled() {
|
|
// Mock paramtable to disable sync mode
|
|
paramtable.Get().CommonCfg.QNFileResourceMode.SwapTempValue("close")
|
|
|
|
// Start observer - no mocks should be called
|
|
suite.observer.Start()
|
|
|
|
// Wait a bit
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// No sync should have been triggered, so no expectations needed
|
|
}
|
|
|
|
func (suite *FileResourceObserverSuite) TestMultipleUpdatesAndNotifications() {
|
|
// Test multiple rapid updates
|
|
for i := 0; i < 5; i++ {
|
|
resources := []*internalpb.FileResourceInfo{
|
|
{
|
|
Id: int64(i + 1),
|
|
Name: "test.file",
|
|
Path: "/test/test.file",
|
|
},
|
|
}
|
|
version := uint64(i + 1)
|
|
|
|
suite.observer.UpdateResources(resources, version)
|
|
|
|
// Verify latest update
|
|
resultResources, resultVersion := suite.observer.getResources()
|
|
suite.Equal(resources, resultResources)
|
|
suite.Equal(version, resultVersion)
|
|
}
|
|
}
|
|
|
|
func TestFileResourceObserverSuite(t *testing.T) {
|
|
suite.Run(t, new(FileResourceObserverSuite))
|
|
}
|