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
2433 lines
74 KiB
Go
2433 lines
74 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 datacoord
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bytedance/mockey"
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"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/datacoord/session"
|
|
"github.com/milvus-io/milvus/internal/metastore"
|
|
"github.com/milvus-io/milvus/internal/storage"
|
|
"github.com/milvus-io/milvus/internal/storagev2/packed"
|
|
"github.com/milvus-io/milvus/pkg/v3/common"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
|
|
"github.com/milvus-io/milvus/pkg/v3/taskcommon"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
// ==================== Stub Implementations ====================
|
|
|
|
// stubCatalog is a simple stub implementation of DataCoordCatalog for testing
|
|
type stubCatalog struct {
|
|
metastore.DataCoordCatalog
|
|
jobs []*datapb.ExternalCollectionRefreshJob
|
|
tasks []*datapb.ExternalCollectionRefreshTask
|
|
alterSegmentErr error
|
|
alteredSegments []*datapb.SegmentInfo
|
|
}
|
|
|
|
func (s *stubCatalog) ListExternalCollectionRefreshJobs(ctx context.Context) ([]*datapb.ExternalCollectionRefreshJob, error) {
|
|
return s.jobs, nil
|
|
}
|
|
|
|
func (s *stubCatalog) ListExternalCollectionRefreshTasks(ctx context.Context) ([]*datapb.ExternalCollectionRefreshTask, error) {
|
|
return s.tasks, nil
|
|
}
|
|
|
|
func (s *stubCatalog) SaveExternalCollectionRefreshJob(ctx context.Context, job *datapb.ExternalCollectionRefreshJob) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *stubCatalog) SaveExternalCollectionRefreshTask(ctx context.Context, task *datapb.ExternalCollectionRefreshTask) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *stubCatalog) DropExternalCollectionRefreshJob(ctx context.Context, jobID typeutil.UniqueID) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *stubCatalog) DropExternalCollectionRefreshTask(ctx context.Context, taskID typeutil.UniqueID) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *stubCatalog) AlterSegments(ctx context.Context, newSegments []*datapb.SegmentInfo, binlogs ...metastore.BinlogsIncrement) error {
|
|
s.alteredSegments = append([]*datapb.SegmentInfo(nil), newSegments...)
|
|
return s.alterSegmentErr
|
|
}
|
|
|
|
// stubAllocator is a simple stub implementation of Allocator for testing
|
|
type stubAllocator struct {
|
|
allocator.Allocator
|
|
nextID int64
|
|
}
|
|
|
|
func (s *stubAllocator) AllocID(ctx context.Context) (typeutil.UniqueID, error) {
|
|
s.nextID++
|
|
return s.nextID, nil
|
|
}
|
|
|
|
func (s *stubAllocator) AllocTimestamp(ctx context.Context) (typeutil.Timestamp, error) {
|
|
return uint64(time.Now().UnixNano()), nil
|
|
}
|
|
|
|
func (s *stubAllocator) AllocN(n int64) (typeutil.UniqueID, typeutil.UniqueID, error) {
|
|
start := s.nextID + 1
|
|
s.nextID += n
|
|
return start, s.nextID + 1, nil
|
|
}
|
|
|
|
// stubCluster is a simple stub implementation of Cluster for testing
|
|
type stubCluster struct {
|
|
session.Cluster
|
|
refreshReq *datapb.RefreshExternalCollectionTaskRequest
|
|
}
|
|
|
|
func (s *stubCluster) CreateRefreshExternalCollectionTask(nodeID int64, req *datapb.RefreshExternalCollectionTaskRequest) error {
|
|
s.refreshReq = req
|
|
return nil
|
|
}
|
|
|
|
func (s *stubCluster) QueryRefreshExternalCollectionTask(nodeID int64, taskID int64) (*datapb.RefreshExternalCollectionTaskResponse, error) {
|
|
return &datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
}, nil
|
|
}
|
|
|
|
func (s *stubCluster) DropRefreshExternalCollectionTask(nodeID int64, taskID int64) error {
|
|
return nil
|
|
}
|
|
|
|
// ==================== Helper Functions ====================
|
|
|
|
// newTestCollections creates a collections map with a single external collection
|
|
// that has one VChannel and one partition, as expected by SetJobInfo.
|
|
func newTestCollections(collectionID int64) *typeutil.ConcurrentMap[UniqueID, *collectionInfo] {
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
collections.Insert(collectionID, &collectionInfo{
|
|
ID: collectionID,
|
|
VChannelNames: []string{"by-dev-rootcoord-dml_0_v1"},
|
|
Partitions: []int64{1},
|
|
})
|
|
return collections
|
|
}
|
|
|
|
func newTestExternalRefreshSegment(segmentID, collectionID, numRows int64) *datapb.SegmentInfo {
|
|
return &datapb.SegmentInfo{
|
|
ID: segmentID,
|
|
CollectionID: collectionID,
|
|
NumOfRows: numRows,
|
|
StorageVersion: 3,
|
|
ManifestPath: `{"base_path":"new","ver":1}`,
|
|
SchemaVersion: 1,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: segmentID,
|
|
EntriesNum: numRows,
|
|
MemorySize: numRows,
|
|
LogSize: numRows,
|
|
}},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func createTestRefreshTaskWithStubs(t *testing.T, taskID, jobID, collectionID int64) (*refreshExternalCollectionTask, *externalCollectionRefreshMeta) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: taskID,
|
|
JobId: jobID,
|
|
CollectionId: collectionID,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
return task, refreshMeta
|
|
}
|
|
|
|
func createTestRefreshTaskWithMetaAndStubs(t *testing.T, taskID, jobID, collectionID int64, mt *meta, refreshMeta *externalCollectionRefreshMeta) *refreshExternalCollectionTask {
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: taskID,
|
|
JobId: jobID,
|
|
CollectionId: collectionID,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
return newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
}
|
|
|
|
// ==================== Basic Interface Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_NewTask(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.NotNil(t, task)
|
|
assert.Equal(t, int64(1001), task.GetTaskId())
|
|
assert.Equal(t, int64(1), task.GetJobId())
|
|
assert.Equal(t, int64(100), task.GetCollectionId())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_GetTaskID(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.Equal(t, int64(1001), task.GetTaskID())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_GetTaskType(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.Equal(t, taskcommon.RefreshExternalCollection, task.GetTaskType())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_GetTaskState(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.Equal(t, indexpb.JobState_JobStateInit, task.GetTaskState())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_GetTaskSlot(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.Equal(t, int64(1), task.GetTaskSlot())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_GetTaskVersion(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
assert.Equal(t, int64(0), task.GetTaskVersion())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_SetTaskTime(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
|
|
now := time.Now()
|
|
task.SetTaskTime(taskcommon.TimeQueue, now)
|
|
|
|
gotTime := task.GetTaskTime(taskcommon.TimeQueue)
|
|
assert.Equal(t, now.Unix(), gotTime.Unix())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_SetState(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
|
|
task.SetState(indexpb.JobState_JobStateInProgress, "")
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, task.GetState())
|
|
|
|
task.SetState(indexpb.JobState_JobStateFailed, "some error")
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, task.GetState())
|
|
assert.Equal(t, "some error", task.GetFailReason())
|
|
}
|
|
|
|
// ==================== ValidateSource Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_ValidateSource(t *testing.T) {
|
|
t.Run("skip_when_mt_is_nil", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
// task.mt is nil by default
|
|
err := task.validateSource()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("job_not_found", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 999, 100)
|
|
// Create a meta but don't add the job
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
mt := &meta{collections: collections}
|
|
task.mt = mt
|
|
|
|
// Job with ID 999 doesn't exist
|
|
err := task.validateSource()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "job 999 not found")
|
|
|
|
// Now add a different job
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
// Still should fail because task's jobID is 999
|
|
err = task.validateSource()
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("source_matches", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
mt := &meta{collections: collections}
|
|
task.mt = mt
|
|
|
|
// Add job with matching source
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err := refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
err = task.validateSource()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("source_mismatch", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
mt := &meta{collections: collections}
|
|
task.mt = mt
|
|
|
|
// Add job with different source
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
ExternalSource: "s3://different/path",
|
|
ExternalSpec: "delta",
|
|
}
|
|
err := refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
err = task.validateSource()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "task source mismatch")
|
|
})
|
|
}
|
|
|
|
// ==================== UpdateStateWithMeta Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_UpdateStateWithMeta(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
|
|
// Add task to meta first
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
err = task.UpdateStateWithMeta(indexpb.JobState_JobStateInProgress, "")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, task.GetState())
|
|
|
|
// Verify meta was updated
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, metaTask.GetState())
|
|
})
|
|
|
|
t.Run("task_not_found", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
// Don't add task to meta
|
|
|
|
err := task.UpdateStateWithMeta(indexpb.JobState_JobStateInProgress, "")
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
// Eager synchronous contract: when the task transitions to a terminal
|
|
// state, processFinishedJob must fire BEFORE UpdateStateWithMeta returns,
|
|
// with the correct jobID. This guarantees callers polling progress see a
|
|
// consistent state (schema update has already been applied).
|
|
t.Run("terminal_finished_fires_process_finished_job_synchronously", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 42, 100)
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
var callbackJobID int64
|
|
callbackCount := 0
|
|
task.processFinishedJob = func(jobID int64) {
|
|
callbackJobID = jobID
|
|
callbackCount++
|
|
}
|
|
|
|
err = task.UpdateStateWithMeta(indexpb.JobState_JobStateFinished, "")
|
|
assert.NoError(t, err)
|
|
|
|
// Callback fired exactly once, with correct jobID, before return.
|
|
assert.Equal(t, 1, callbackCount, "processFinishedJob must fire exactly once")
|
|
assert.Equal(t, int64(42), callbackJobID, "processFinishedJob must receive the task's jobID")
|
|
})
|
|
|
|
t.Run("terminal_failed_fires_process_finished_job", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 7, 100)
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
called := false
|
|
task.processFinishedJob = func(jobID int64) { called = true }
|
|
|
|
err = task.UpdateStateWithMeta(indexpb.JobState_JobStateFailed, "worker crashed")
|
|
assert.NoError(t, err)
|
|
assert.True(t, called, "processFinishedJob must also fire on Failed transitions")
|
|
})
|
|
|
|
t.Run("non_terminal_does_not_fire_process_finished_job", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
called := false
|
|
task.processFinishedJob = func(jobID int64) { called = true }
|
|
|
|
err = task.UpdateStateWithMeta(indexpb.JobState_JobStateInProgress, "")
|
|
assert.NoError(t, err)
|
|
assert.False(t, called, "processFinishedJob must NOT fire on non-terminal transitions")
|
|
})
|
|
|
|
t.Run("nil_process_finished_job_no_panic", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
// processFinishedJob unset (nil) — test-fixture case.
|
|
task.processFinishedJob = nil
|
|
assert.NotPanics(t, func() {
|
|
_ = task.UpdateStateWithMeta(indexpb.JobState_JobStateFinished, "")
|
|
})
|
|
})
|
|
}
|
|
|
|
// ==================== UpdateProgressWithMeta Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_UpdateProgressWithMeta(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
task, refreshMeta := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
|
|
// Add task to meta first
|
|
err := refreshMeta.AddTask(task.ExternalCollectionRefreshTask)
|
|
assert.NoError(t, err)
|
|
|
|
err = task.UpdateProgressWithMeta(50)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(50), task.GetProgress())
|
|
|
|
// Verify meta was updated
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, int64(50), metaTask.GetProgress())
|
|
})
|
|
|
|
t.Run("task_not_found", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
// Don't add task to meta
|
|
|
|
err := task.UpdateProgressWithMeta(50)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// ==================== SetJobInfo Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_SetJobInfo(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("meta_is_nil", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
// task.mt is nil
|
|
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{1, 2},
|
|
UpdatedSegments: []*datapb.SegmentInfo{},
|
|
}
|
|
|
|
err := task.SetJobInfo(ctx, resp)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "meta is nil")
|
|
})
|
|
|
|
t.Run("safety_check_drop_all_segments", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create segments info
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
},
|
|
})
|
|
segments.SetSegment(2, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 2,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
},
|
|
})
|
|
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
task := createTestRefreshTaskWithMetaAndStubs(t, 1001, 1, 100, mt, refreshMeta)
|
|
|
|
// Try to drop all segments without replacement
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{}, // Keep none
|
|
UpdatedSegments: []*datapb.SegmentInfo{}, // Add none
|
|
}
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "safety check failed")
|
|
})
|
|
|
|
t.Run("success_drop_and_add_segments", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create segments info with existing segments
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 500,
|
|
},
|
|
})
|
|
segments.SetSegment(2, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 2,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 600,
|
|
},
|
|
})
|
|
segments.SetSegment(3, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 3,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Dropped, // already dropped
|
|
NumOfRows: 100,
|
|
},
|
|
})
|
|
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
task := createTestRefreshTaskWithMetaAndStubs(t, 1001, 1, 100, mt, refreshMeta)
|
|
|
|
// Keep segment 1, drop segment 2, add segment 10
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{1},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(10, 100, 1000),
|
|
},
|
|
}
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.NoError(t, err)
|
|
newSegment := mt.segments.GetSegment(10)
|
|
assert.NotNil(t, newSegment)
|
|
assert.False(t, newSegment.GetIsSorted())
|
|
assert.Equal(t, commonpb.SegmentState_Flushed, newSegment.GetState())
|
|
assert.Equal(t, "by-dev-rootcoord-dml_0_v1", newSegment.GetInsertChannel())
|
|
assert.Equal(t, int64(1), newSegment.GetPartitionID())
|
|
})
|
|
|
|
t.Run("success_update_existing_segment_manifest", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
PartitionID: 1,
|
|
InsertChannel: "by-dev-rootcoord-dml_0_v1",
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 500,
|
|
ManifestPath: "old-manifest",
|
|
StorageVersion: 3,
|
|
},
|
|
})
|
|
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
task := createTestRefreshTaskWithMetaAndStubs(t, 1001, 1, 100, mt, refreshMeta)
|
|
updated := newTestExternalRefreshSegment(1, 100, 500)
|
|
updated.ManifestPath = "new-manifest"
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
UpdatedSegments: []*datapb.SegmentInfo{updated},
|
|
}
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.NoError(t, err)
|
|
segment := mt.segments.GetSegment(1)
|
|
assert.NotNil(t, segment)
|
|
assert.Equal(t, commonpb.SegmentState_Flushed, segment.GetState())
|
|
assert.Equal(t, "new-manifest", segment.GetManifestPath())
|
|
assert.Equal(t, uint64(0), segment.GetDroppedAt())
|
|
assert.Len(t, catalog.alteredSegments, 1)
|
|
assert.Equal(t, int64(1), catalog.alteredSegments[0].GetID())
|
|
})
|
|
|
|
t.Run("high_drop_ratio_warning", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create 10 segments
|
|
segments := NewSegmentsInfo()
|
|
for i := int64(1); i <= 10; i++ {
|
|
segments.SetSegment(i, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: i,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 100,
|
|
},
|
|
})
|
|
}
|
|
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
task := createTestRefreshTaskWithMetaAndStubs(t, 1001, 1, 100, mt, refreshMeta)
|
|
|
|
// Keep only 1 segment (drop 9 out of 10 = 90% drop ratio, triggers warning)
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{1},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(20, 100, 2000),
|
|
},
|
|
}
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("update_segments_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{
|
|
alterSegmentErr: errors.New("alter segments failed"),
|
|
}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create segments info
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
|
|
alloc := &stubAllocator{}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(1, 100, 1000),
|
|
},
|
|
}
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "alter segments failed")
|
|
})
|
|
}
|
|
|
|
// ==================== CreateTaskOnWorker Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_CreateTaskOnWorker(t *testing.T) {
|
|
t.Run("meta_is_nil", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc) // mt is nil
|
|
|
|
cluster := &stubCluster{}
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "meta is nil")
|
|
})
|
|
|
|
t.Run("update_version_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
// Mock SaveExternalCollectionRefreshTask to return error
|
|
mockSave := mockey.Mock((*stubCatalog).SaveExternalCollectionRefreshTask).Return(errors.New("save failed")).Build()
|
|
defer mockSave.UnPatch()
|
|
|
|
cluster := &stubCluster{}
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task state should remain Init since version update failed
|
|
})
|
|
|
|
t.Run("alloc_segment_ids_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
// Mock AllocN to return error
|
|
mockAllocN := mockey.Mock((*stubAllocator).AllocN).Return(int64(0), int64(0), errors.New("alloc batch failed")).Build()
|
|
defer mockAllocN.UnPatch()
|
|
|
|
cluster := &stubCluster{}
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "alloc batch failed")
|
|
})
|
|
|
|
t.Run("collection_not_found", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as failed since collection is not in meta
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "collection 100 not found")
|
|
})
|
|
|
|
t.Run("create_task_on_worker_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
collections.Insert(100, &collectionInfo{
|
|
ID: 100,
|
|
Schema: &schemapb.CollectionSchema{Name: "test_coll"},
|
|
Partitions: []int64{10},
|
|
})
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: collections,
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock CreateRefreshExternalCollectionTask to return error
|
|
mockCreate := mockey.Mock((*stubCluster).CreateRefreshExternalCollectionTask).Return(errors.New("create task failed")).Build()
|
|
defer mockCreate.UnPatch()
|
|
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "create task failed")
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
},
|
|
})
|
|
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
collections.Insert(100, &collectionInfo{
|
|
ID: 100,
|
|
Schema: &schemapb.CollectionSchema{Name: "test_coll"},
|
|
Partitions: []int64{10},
|
|
})
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: collections,
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as in progress
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, metaTask.GetState())
|
|
assert.NotNil(t, cluster.refreshReq)
|
|
assert.Equal(t, int64(10), cluster.refreshReq.GetPartitionID())
|
|
})
|
|
}
|
|
|
|
// ==================== QueryTaskOnWorker Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker(t *testing.T) {
|
|
t.Run("job_not_found", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "job canceled")
|
|
})
|
|
|
|
t.Run("job_already_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add a failed job
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateFailed,
|
|
FailReason: "timeout",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "job canceled")
|
|
})
|
|
|
|
t.Run("query_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add active job
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock QueryRefreshExternalCollectionTask to return error
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(nil, errors.New("query failed")).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "query task failed")
|
|
})
|
|
|
|
t.Run("task_in_progress", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add active job
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// stubCluster.QueryRefreshExternalCollectionTask returns InProgress by default
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should remain in progress
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, metaTask.GetState())
|
|
})
|
|
|
|
t.Run("task_failed_on_worker", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add active job
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock QueryRefreshExternalCollectionTask to return failed state
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFailed,
|
|
FailReason: "worker error",
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "worker error")
|
|
})
|
|
|
|
t.Run("task_finished_success", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add active job with matching source
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
// Create segments and meta
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 500,
|
|
},
|
|
})
|
|
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock QueryRefreshExternalCollectionTask to return Finished with response
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
KeptSegments: []int64{1},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(10, 100, 1000),
|
|
},
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
// Mock UpdateSegmentsInfo to succeed
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).Return(nil).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as finished
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFinished, metaTask.GetState())
|
|
})
|
|
|
|
t.Run("task_finished_validate_source_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add job with DIFFERENT source (to trigger validateSource failure)
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://different/path",
|
|
ExternalSpec: "delta",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
collections := typeutil.NewConcurrentMap[UniqueID, *collectionInfo]()
|
|
mt := &meta{
|
|
segments: NewSegmentsInfo(),
|
|
collections: collections,
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock QueryRefreshExternalCollectionTask to return Finished
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed due to source mismatch
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "task source mismatch")
|
|
})
|
|
|
|
// Part 8 cross-bucket relaxed JobStateNone/JobStateInit to mean "not yet
|
|
// picked up by the worker scheduler" (benign no-op) instead of "task not
|
|
// found" (failure). JobStateRetry is the only state still treated as
|
|
// unexpected and forces Failed.
|
|
t.Run("task_state_none_no_op", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Worker reports JobStateNone — task hasn't been picked up yet.
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateNone,
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task state should remain InProgress (no-op), not Failed.
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, metaTask.GetState())
|
|
assert.Empty(t, metaTask.GetFailReason())
|
|
})
|
|
|
|
// JobStateInit shares the no-op branch with JobStateNone/JobStateInProgress:
|
|
// worker has the task but hasn't started execution yet. Task must stay
|
|
// InProgress from DataCoord's view, not be marked Failed.
|
|
t.Run("task_state_init_no_op", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Worker reports JobStateInit — task accepted but not yet running.
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateInit,
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task state must remain InProgress (no-op), not Failed.
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateInProgress, metaTask.GetState())
|
|
assert.Empty(t, metaTask.GetFailReason())
|
|
})
|
|
|
|
t.Run("task_state_retry_marks_failed", func(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: `{"format":"parquet"}`,
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// JobStateRetry is the only state still considered unexpected.
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateRetry,
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "unexpected state")
|
|
})
|
|
}
|
|
|
|
// ==================== QueryTaskOnWorker Additional Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker_FinishedSuccess(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add active job with matching source
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock QueryRefreshExternalCollectionTask to return Finished with response
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
KeptSegments: []int64{},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(1, 100, 1000),
|
|
},
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
// Mock UpdateSegmentsInfo to succeed
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).Return(nil).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as Finished
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFinished, metaTask.GetState())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker_DelaysSegmentUpdateUntilJobFinished(t *testing.T) {
|
|
ctx := context.Background()
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
assert.NoError(t, refreshMeta.AddJob(job))
|
|
|
|
task1 := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
task2 := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1002,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
assert.NoError(t, refreshMeta.AddTask(task1))
|
|
assert.NoError(t, refreshMeta.AddTask(task2))
|
|
assert.NoError(t, refreshMeta.AddTaskIDToJob(1, 1001))
|
|
assert.NoError(t, refreshMeta.AddTaskIDToJob(1, 1002))
|
|
|
|
mt := &meta{
|
|
catalog: catalog,
|
|
segments: NewSegmentsInfo(),
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
cluster := &stubCluster{}
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
KeptSegments: []int64{},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(10, 100, 7),
|
|
},
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
updateCalls := 0
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).To(func(_ *meta, _ context.Context, _ ...UpdateOperator) error {
|
|
updateCalls++
|
|
return nil
|
|
}).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
task := newRefreshExternalCollectionTask(task1, refreshMeta, mt, &stubAllocator{nextID: 99999})
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFinished, metaTask.GetState())
|
|
assert.Equal(t, 0, updateCalls)
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_UpsertExistingSegment(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
partitionID := int64(1)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
oldSeg := &datapb.SegmentInfo{
|
|
ID: segmentID,
|
|
CollectionID: collectionID,
|
|
PartitionID: partitionID,
|
|
InsertChannel: "by-dev-rootcoord-dml_0_v1",
|
|
NumOfRows: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
StorageVersion: 3,
|
|
Level: datapb.SegmentLevel_L1,
|
|
IsSorted: true,
|
|
ManifestPath: `{"base_path":"old","ver":1}`,
|
|
SchemaVersion: 3,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
ChildFields: []int64{100, 101, 102},
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1000,
|
|
LogSize: 1000,
|
|
}},
|
|
}},
|
|
}
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(oldSeg))
|
|
|
|
patched := proto.Clone(oldSeg).(*datapb.SegmentInfo)
|
|
patched.ManifestPath = `{"base_path":"old","ver":2}`
|
|
patched.SchemaVersion = 4
|
|
patched.Level = datapb.SegmentLevel_L0
|
|
patched.IsSorted = false
|
|
patched.Binlogs = []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
ChildFields: []int64{100, 101, 102, 103},
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1400,
|
|
LogSize: 1400,
|
|
}},
|
|
}}
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{patched},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
got := mt.segments.GetSegment(segmentID)
|
|
assert.NotNil(t, got)
|
|
assert.Equal(t, commonpb.SegmentState_Flushed, got.GetState())
|
|
assert.Equal(t, int64(100), got.GetNumOfRows())
|
|
assert.Equal(t, datapb.SegmentLevel_L1, got.GetLevel())
|
|
assert.True(t, got.GetIsSorted())
|
|
assert.Equal(t, `{"base_path":"old","ver":2}`, got.GetManifestPath())
|
|
assert.Equal(t, int32(4), got.GetSchemaVersion())
|
|
assert.ElementsMatch(t, []int64{100, 101, 102, 103}, got.GetBinlogs()[0].GetChildFields())
|
|
}
|
|
|
|
func TestApplyExternalRefreshPatchClearsStatsPlaceholders(t *testing.T) {
|
|
oldManifest := packed.MarshalManifestPath("files/insert_log/100/200/300", 1)
|
|
newManifest := packed.MarshalManifestPath("files/insert_log/100/200/300", 2)
|
|
|
|
oldSeg := &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 300,
|
|
CollectionID: 100,
|
|
PartitionID: 200,
|
|
NumOfRows: 1000,
|
|
ManifestPath: oldManifest,
|
|
StorageVersion: storage.StorageV3,
|
|
SchemaVersion: 1,
|
|
TextStatsLogs: map[int64]*datapb.TextIndexStats{
|
|
500: {
|
|
FieldID: 500,
|
|
Version: 1,
|
|
BuildID: 10,
|
|
Files: []string{"files/insert_log/100/200/300/_stats/text_index.500/tokenizer.json"},
|
|
},
|
|
},
|
|
JsonKeyStats: map[int64]*datapb.JsonKeyStats{
|
|
500: {
|
|
FieldID: 500,
|
|
Version: 1,
|
|
BuildID: 10,
|
|
Files: []string{"shared_key_index/.managed.json_0"},
|
|
JsonKeyStatsDataFormat: common.JSONStatsDataFormatVersion,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
incoming := &datapb.SegmentInfo{
|
|
ID: 300,
|
|
CollectionID: 100,
|
|
PartitionID: 200,
|
|
NumOfRows: 1000,
|
|
ManifestPath: newManifest,
|
|
StorageVersion: storage.StorageV3,
|
|
SchemaVersion: 2,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
ChildFields: []int64{100, 500},
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 300,
|
|
EntriesNum: 1000,
|
|
MemorySize: 4096,
|
|
LogSize: 4096,
|
|
}},
|
|
}},
|
|
}
|
|
|
|
patched := applyExternalRefreshPatch(oldSeg, incoming)
|
|
assert.Equal(t, newManifest, patched.GetManifestPath())
|
|
assert.Empty(t, patched.GetTextStatsLogs())
|
|
assert.Empty(t, patched.GetJsonKeyStats())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectPatchRowCountChange(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
oldSeg := &datapb.SegmentInfo{
|
|
ID: segmentID,
|
|
CollectionID: collectionID,
|
|
PartitionID: 1,
|
|
InsertChannel: "by-dev-rootcoord-dml_0_v1",
|
|
NumOfRows: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
StorageVersion: 3,
|
|
ManifestPath: `{"base_path":"old","ver":1}`,
|
|
SchemaVersion: 3,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1000,
|
|
LogSize: 1000,
|
|
}},
|
|
}},
|
|
}
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(oldSeg))
|
|
|
|
patched := proto.Clone(oldSeg).(*datapb.SegmentInfo)
|
|
patched.NumOfRows = 101
|
|
patched.ManifestPath = `{"base_path":"old","ver":2}`
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{patched},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "row count changed")
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectDroppedSegmentPatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
oldSeg := &datapb.SegmentInfo{
|
|
ID: segmentID,
|
|
CollectionID: collectionID,
|
|
PartitionID: 1,
|
|
InsertChannel: "by-dev-rootcoord-dml_0_v1",
|
|
NumOfRows: 100,
|
|
State: commonpb.SegmentState_Dropped,
|
|
StorageVersion: 3,
|
|
ManifestPath: `{"base_path":"old","ver":1}`,
|
|
SchemaVersion: 3,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
ChildFields: []int64{100, 101, 102},
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1000,
|
|
LogSize: 1000,
|
|
}},
|
|
}},
|
|
}
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(oldSeg))
|
|
|
|
patched := proto.Clone(oldSeg).(*datapb.SegmentInfo)
|
|
patched.ManifestPath = `{"base_path":"old","ver":2}`
|
|
patched.SchemaVersion = 4
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{patched},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cannot patch dropped segment")
|
|
assert.Equal(t, commonpb.SegmentState_Dropped, mt.segments.GetSegment(segmentID).GetState())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectNewSegmentCollectionMismatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{{
|
|
ID: 10,
|
|
CollectionID: collectionID + 1,
|
|
NumOfRows: 100,
|
|
StorageVersion: 3,
|
|
ManifestPath: `{"base_path":"new","ver":1}`,
|
|
SchemaVersion: 1,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1000,
|
|
LogSize: 1000,
|
|
}},
|
|
}},
|
|
}},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "collection mismatch")
|
|
assert.Nil(t, mt.segments.GetSegment(10))
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectNewSegmentEmptyManifest(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
seg := newTestExternalRefreshSegment(10, collectionID, 100)
|
|
seg.ManifestPath = ""
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{seg},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "empty manifest path")
|
|
assert.Nil(t, mt.segments.GetSegment(10))
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectSegmentIDFromOtherCollection(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(&datapb.SegmentInfo{
|
|
ID: segmentID,
|
|
CollectionID: collectionID + 1,
|
|
PartitionID: 1,
|
|
InsertChannel: "by-dev-rootcoord-dml_1_v1",
|
|
NumOfRows: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
StorageVersion: 3,
|
|
ManifestPath: `{"base_path":"other","ver":1}`,
|
|
SchemaVersion: 3,
|
|
Binlogs: []*datapb.FieldBinlog{{
|
|
FieldID: 0,
|
|
Binlogs: []*datapb.Binlog{{
|
|
LogID: 10,
|
|
EntriesNum: 100,
|
|
MemorySize: 1000,
|
|
LogSize: 1000,
|
|
}},
|
|
}},
|
|
}))
|
|
incoming := newTestExternalRefreshSegment(segmentID, collectionID, 100)
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{incoming},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "collection mismatch")
|
|
got := mt.segments.GetSegment(segmentID)
|
|
assert.NotNil(t, got)
|
|
assert.Equal(t, collectionID+1, got.GetCollectionID())
|
|
assert.Equal(t, `{"base_path":"other","ver":1}`, got.GetManifestPath())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectInvalidKeptSegment(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
mt.segments.SetSegment(1, NewSegmentInfo(&datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: collectionID,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 100,
|
|
}))
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
[]int64{999},
|
|
nil,
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "kept segment 999 not found")
|
|
assert.Equal(t, commonpb.SegmentState_Flushed, mt.segments.GetSegment(1).GetState())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectKeptSegmentFromOtherCollection(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
mt.segments.SetSegment(10, NewSegmentInfo(&datapb.SegmentInfo{
|
|
ID: 10,
|
|
CollectionID: collectionID + 1,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 100,
|
|
}))
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
[]int64{10},
|
|
nil,
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "collection mismatch")
|
|
assert.Equal(t, collectionID+1, mt.segments.GetSegment(10).GetCollectionID())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectDroppedKeptSegment(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
mt.segments.SetSegment(10, NewSegmentInfo(&datapb.SegmentInfo{
|
|
ID: 10,
|
|
CollectionID: collectionID,
|
|
State: commonpb.SegmentState_Dropped,
|
|
NumOfRows: 100,
|
|
}))
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
[]int64{10},
|
|
nil,
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cannot keep dropped segment")
|
|
assert.Equal(t, commonpb.SegmentState_Dropped, mt.segments.GetSegment(10).GetState())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_NormalizeNewSegmentCollection(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
seg := newTestExternalRefreshSegment(10, 0, 100)
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{seg},
|
|
)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(0), seg.GetCollectionID())
|
|
assert.Equal(t, int64(0), seg.GetPartitionID())
|
|
assert.Empty(t, seg.GetInsertChannel())
|
|
|
|
got := mt.segments.GetSegment(10)
|
|
assert.NotNil(t, got)
|
|
assert.Equal(t, collectionID, got.GetCollectionID())
|
|
assert.Equal(t, int64(1), got.GetPartitionID())
|
|
assert.Equal(t, "by-dev-rootcoord-dml_0_v1", got.GetInsertChannel())
|
|
assert.Equal(t, commonpb.SegmentState_Flushed, got.GetState())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectPatchBinlogRowCountMismatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
oldSeg := newTestExternalRefreshSegment(segmentID, collectionID, 100)
|
|
oldSeg.State = commonpb.SegmentState_Flushed
|
|
oldSeg.PartitionID = 1
|
|
oldSeg.InsertChannel = "by-dev-rootcoord-dml_0_v1"
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(oldSeg))
|
|
|
|
patched := proto.Clone(oldSeg).(*datapb.SegmentInfo)
|
|
patched.ManifestPath = `{"base_path":"old","ver":2}`
|
|
patched.Binlogs[0].Binlogs[0].EntriesNum = 99
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{patched},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "binlog row count mismatch")
|
|
assert.Equal(t, `{"base_path":"new","ver":1}`, mt.segments.GetSegment(segmentID).GetManifestPath())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectNewBinlogRowCountMismatch(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
seg := newTestExternalRefreshSegment(10, collectionID, 100)
|
|
seg.Binlogs[0].Binlogs[0].EntriesNum = 99
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{seg},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "binlog row count mismatch")
|
|
assert.Nil(t, mt.segments.GetSegment(10))
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectPatchEmptyNestedBinlogs(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
segmentID := int64(10)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
oldSeg := newTestExternalRefreshSegment(segmentID, collectionID, 100)
|
|
oldSeg.State = commonpb.SegmentState_Flushed
|
|
oldSeg.PartitionID = 1
|
|
oldSeg.InsertChannel = "by-dev-rootcoord-dml_0_v1"
|
|
mt.segments.SetSegment(segmentID, NewSegmentInfo(oldSeg))
|
|
|
|
patched := proto.Clone(oldSeg).(*datapb.SegmentInfo)
|
|
patched.ManifestPath = `{"base_path":"old","ver":2}`
|
|
patched.Binlogs = []*datapb.FieldBinlog{{FieldID: 0}}
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{patched},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "binlog row count mismatch")
|
|
assert.Equal(t, `{"base_path":"new","ver":1}`, mt.segments.GetSegment(segmentID).GetManifestPath())
|
|
}
|
|
|
|
func TestApplyExternalCollectionSegmentUpdate_RejectNewEmptyNestedBinlogs(t *testing.T) {
|
|
ctx := context.Background()
|
|
collectionID := int64(100)
|
|
mt := &meta{
|
|
collections: newTestCollections(collectionID),
|
|
segments: NewSegmentsInfo(),
|
|
catalog: &stubCatalog{},
|
|
}
|
|
seg := newTestExternalRefreshSegment(10, collectionID, 100)
|
|
seg.Binlogs = []*datapb.FieldBinlog{{FieldID: 0}}
|
|
|
|
err := applyExternalCollectionSegmentUpdate(
|
|
ctx,
|
|
mt,
|
|
collectionID,
|
|
nil,
|
|
[]*datapb.SegmentInfo{seg},
|
|
)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "binlog row count mismatch")
|
|
assert.Nil(t, mt.segments.GetSegment(10))
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker_FinishedValidateSourceFailed(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add job with DIFFERENT source than task
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://different/path",
|
|
ExternalSpec: "delta",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock query to return Finished
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
// Task should be marked as failed due to source mismatch
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "task source mismatch")
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker_FinishedPersistsResultWithoutSegmentMeta(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Add job with matching source
|
|
job := &datapb.ExternalCollectionRefreshJob{
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddJob(job)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 1,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
// Task has nil mt. Finished task handling must still succeed because
|
|
// segment metadata is applied later at job level, not by this task.
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc) // mt is nil
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock query to return Finished
|
|
mockQuery := mockey.Mock((*stubCluster).QueryRefreshExternalCollectionTask).Return(&datapb.RefreshExternalCollectionTaskResponse{
|
|
State: indexpb.JobState_JobStateFinished,
|
|
KeptSegments: []int64{10},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(20, 100, 7),
|
|
},
|
|
}, nil).Build()
|
|
defer mockQuery.UnPatch()
|
|
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFinished, metaTask.GetState())
|
|
assert.Empty(t, metaTask.GetFailReason())
|
|
assert.Equal(t, []int64{10}, metaTask.GetKeptSegments())
|
|
assert.Len(t, metaTask.GetUpdatedSegments(), 1)
|
|
assert.Equal(t, int64(20), metaTask.GetUpdatedSegments()[0].GetID())
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_QueryTaskOnWorker_JobNotFoundNodeIdZero(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
NodeId: 0, // Not assigned to any node
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, nil, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Job doesn't exist, nodeId is 0 so DropRefreshExternalCollectionTask should NOT be called
|
|
task.QueryTaskOnWorker(cluster)
|
|
|
|
metaTask := refreshMeta.GetTask(1001)
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, metaTask.GetState())
|
|
assert.Contains(t, metaTask.GetFailReason(), "job canceled")
|
|
}
|
|
|
|
// ==================== SetJobInfo Additional Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_SetJobInfo_SuccessWithSegments(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create existing segments
|
|
segments := NewSegmentsInfo()
|
|
segments.SetSegment(1, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 1,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 1000,
|
|
},
|
|
})
|
|
segments.SetSegment(2, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: 2,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 2000,
|
|
},
|
|
})
|
|
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
// Keep segment 1, drop segment 2, add a new segment
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{1},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(999, 100, 3000),
|
|
},
|
|
}
|
|
|
|
// Mock UpdateSegmentsInfo to succeed
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).Return(nil).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_SetJobInfo_HighDropRatioWarning(t *testing.T) {
|
|
ctx := context.Background()
|
|
paramtable.Init()
|
|
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
// Create 10 existing segments
|
|
segments := NewSegmentsInfo()
|
|
for i := int64(1); i <= 10; i++ {
|
|
segments.SetSegment(i, &SegmentInfo{
|
|
SegmentInfo: &datapb.SegmentInfo{
|
|
ID: i,
|
|
CollectionID: 100,
|
|
State: commonpb.SegmentState_Flushed,
|
|
NumOfRows: 1000,
|
|
},
|
|
})
|
|
}
|
|
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
// Keep only 1 out of 10 segments (90% drop ratio) → triggers warning
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{1},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(999, 100, 5000),
|
|
},
|
|
}
|
|
|
|
// Mock UpdateSegmentsInfo to succeed
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).Return(nil).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRefreshExternalCollectionTask_SetJobInfo_UpdateSegmentsInfoFailed(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(ctx, catalog)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: newTestCollections(100),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInProgress,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
resp := &datapb.RefreshExternalCollectionTaskResponse{
|
|
KeptSegments: []int64{},
|
|
UpdatedSegments: []*datapb.SegmentInfo{
|
|
newTestExternalRefreshSegment(1, 100, 1000),
|
|
},
|
|
}
|
|
|
|
// Mock UpdateSegmentsInfo to fail
|
|
mockUpdate := mockey.Mock((*meta).UpdateSegmentsInfo).Return(errors.New("update segments failed")).Build()
|
|
defer mockUpdate.UnPatch()
|
|
|
|
err = task.SetJobInfo(ctx, resp)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "update segments failed")
|
|
}
|
|
|
|
// ==================== CreateTaskOnWorker Additional Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_CreateTaskOnWorker_TaskNotFoundAfterVersionUpdate(t *testing.T) {
|
|
catalog := &stubCatalog{}
|
|
refreshMeta, err := newExternalCollectionRefreshMeta(context.Background(), catalog)
|
|
assert.NoError(t, err)
|
|
|
|
protoTask := &datapb.ExternalCollectionRefreshTask{
|
|
TaskId: 1001,
|
|
JobId: 1,
|
|
CollectionId: 100,
|
|
State: indexpb.JobState_JobStateInit,
|
|
ExternalSource: "s3://bucket/path",
|
|
ExternalSpec: "iceberg",
|
|
}
|
|
err = refreshMeta.AddTask(protoTask)
|
|
assert.NoError(t, err)
|
|
|
|
segments := NewSegmentsInfo()
|
|
mt := &meta{
|
|
segments: segments,
|
|
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
|
|
}
|
|
|
|
alloc := &stubAllocator{nextID: 99999}
|
|
task := newRefreshExternalCollectionTask(protoTask, refreshMeta, mt, alloc)
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock GetTask to return nil (task disappears after version update)
|
|
mockGetTask := mockey.Mock((*externalCollectionRefreshMeta).GetTask).Return(nil).Build()
|
|
defer mockGetTask.UnPatch()
|
|
|
|
task.CreateTaskOnWorker(1, cluster)
|
|
|
|
// Task should be marked as failed
|
|
// Note: since GetTask is mocked to return nil, we check the in-memory state
|
|
assert.Equal(t, indexpb.JobState_JobStateFailed, task.GetState())
|
|
assert.Contains(t, task.GetFailReason(), "not found after version update")
|
|
}
|
|
|
|
// ==================== DropTaskOnWorker Tests ====================
|
|
|
|
func TestRefreshExternalCollectionTask_DropTaskOnWorker(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
task.NodeId = 1
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
task.DropTaskOnWorker(cluster)
|
|
// No error expected
|
|
})
|
|
|
|
t.Run("drop_failed", func(t *testing.T) {
|
|
task, _ := createTestRefreshTaskWithStubs(t, 1001, 1, 100)
|
|
task.NodeId = 1
|
|
|
|
cluster := &stubCluster{}
|
|
|
|
// Mock DropRefreshExternalCollectionTask to return error
|
|
mockDrop := mockey.Mock((*stubCluster).DropRefreshExternalCollectionTask).Return(errors.New("drop failed")).Build()
|
|
defer mockDrop.UnPatch()
|
|
|
|
task.DropTaskOnWorker(cluster)
|
|
// Error is logged but not returned
|
|
})
|
|
}
|