// 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" "math/rand" "testing" "time" "github.com/cockroachdb/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus-proto/go-api/v2/msgpb" broker2 "github.com/milvus-io/milvus/internal/datacoord/broker" "github.com/milvus-io/milvus/internal/metastore/mocks" "github.com/milvus-io/milvus/internal/proto/datapb" "github.com/milvus-io/milvus/internal/proto/internalpb" "github.com/milvus-io/milvus/pkg/util/tsoutil" ) type ImportCheckerSuite struct { suite.Suite jobID int64 imeta ImportMeta checker *importChecker } func (s *ImportCheckerSuite) SetupTest() { catalog := mocks.NewDataCoordCatalog(s.T()) catalog.EXPECT().ListImportJobs().Return(nil, nil) catalog.EXPECT().ListPreImportTasks().Return(nil, nil) catalog.EXPECT().ListImportTasks().Return(nil, nil) catalog.EXPECT().ListSegments(mock.Anything).Return(nil, nil) catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil) catalog.EXPECT().ListIndexes(mock.Anything).Return(nil, nil) catalog.EXPECT().ListSegmentIndexes(mock.Anything).Return(nil, nil) cluster := NewMockCluster(s.T()) alloc := NewNMockAllocator(s.T()) imeta, err := NewImportMeta(catalog) s.NoError(err) s.imeta = imeta meta, err := newMeta(context.TODO(), catalog, nil) s.NoError(err) broker := broker2.NewMockBroker(s.T()) sm := NewMockManager(s.T()) checker := NewImportChecker(meta, broker, cluster, alloc, sm, imeta).(*importChecker) s.checker = checker job := &importJob{ ImportJob: &datapb.ImportJob{ JobID: 0, CollectionID: 1, PartitionIDs: []int64{2}, Vchannels: []string{"ch0"}, State: internalpb.ImportJobState_Pending, TimeoutTs: 1000, CleanupTs: tsoutil.GetCurrentTime(), Files: []*internalpb.ImportFile{ { Id: 1, Paths: []string{"a.json"}, }, { Id: 2, Paths: []string{"b.json"}, }, { Id: 3, Paths: []string{"c.json"}, }, }, }, } catalog.EXPECT().SaveImportJob(mock.Anything).Return(nil) err = s.imeta.AddJob(job) s.NoError(err) s.jobID = job.GetJobID() } func (s *ImportCheckerSuite) TestLogStats() { catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) catalog.EXPECT().SaveImportTask(mock.Anything).Return(nil) pit1 := &preImportTask{ PreImportTask: &datapb.PreImportTask{ JobID: s.jobID, TaskID: 1, State: datapb.ImportTaskStateV2_Failed, }, } err := s.imeta.AddTask(pit1) s.NoError(err) it1 := &importTask{ ImportTaskV2: &datapb.ImportTaskV2{ JobID: s.jobID, TaskID: 2, SegmentIDs: []int64{10, 11, 12}, State: datapb.ImportTaskStateV2_Pending, }, } err = s.imeta.AddTask(it1) s.NoError(err) s.checker.LogStats() } func (s *ImportCheckerSuite) TestCheckJob() { job := s.imeta.GetJob(s.jobID) // test checkPendingJob alloc := s.checker.alloc.(*NMockAllocator) alloc.EXPECT().allocN(mock.Anything).RunAndReturn(func(n int64) (int64, int64, error) { id := rand.Int63() return id, id + n, nil }) catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) s.checker.checkPendingJob(job) preimportTasks := s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(PreImportTaskType)) s.Equal(2, len(preimportTasks)) s.Equal(internalpb.ImportJobState_PreImporting, s.imeta.GetJob(job.GetJobID()).GetState()) s.checker.checkPendingJob(job) // no lack preimportTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(PreImportTaskType)) s.Equal(2, len(preimportTasks)) s.Equal(internalpb.ImportJobState_PreImporting, s.imeta.GetJob(job.GetJobID()).GetState()) // test checkPreImportingJob catalog.EXPECT().SaveImportTask(mock.Anything).Return(nil) for _, t := range preimportTasks { err := s.imeta.UpdateTask(t.GetTaskID(), UpdateState(datapb.ImportTaskStateV2_Completed)) s.NoError(err) } s.checker.checkPreImportingJob(job) importTasks := s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(ImportTaskType)) s.Equal(1, len(importTasks)) s.Equal(internalpb.ImportJobState_Importing, s.imeta.GetJob(job.GetJobID()).GetState()) s.checker.checkPreImportingJob(job) // no lack importTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(ImportTaskType)) s.Equal(1, len(importTasks)) s.Equal(internalpb.ImportJobState_Importing, s.imeta.GetJob(job.GetJobID()).GetState()) // test checkImportingJob s.checker.checkImportingJob(job) // not completed s.Equal(internalpb.ImportJobState_Importing, s.imeta.GetJob(job.GetJobID()).GetState()) for _, t := range importTasks { task := s.imeta.GetTask(t.GetTaskID()) for _, id := range task.(*importTask).GetSegmentIDs() { segment := s.checker.meta.GetSegment(id) s.Equal(true, segment.GetIsImporting()) } } catalog.EXPECT().AddSegment(mock.Anything, mock.Anything).Return(nil) catalog.EXPECT().AlterSegments(mock.Anything, mock.Anything).Return(nil) catalog.EXPECT().SaveChannelCheckpoint(mock.Anything, mock.Anything, mock.Anything).Return(nil) for _, t := range importTasks { segment := &SegmentInfo{ SegmentInfo: &datapb.SegmentInfo{ ID: rand.Int63(), State: commonpb.SegmentState_Flushed, IsImporting: true, InsertChannel: "ch0", }, } err := s.checker.meta.AddSegment(context.Background(), segment) s.NoError(err) err = s.imeta.UpdateTask(t.GetTaskID(), UpdateState(datapb.ImportTaskStateV2_Completed), UpdateSegmentIDs([]int64{segment.GetID()})) s.NoError(err) err = s.checker.meta.UpdateChannelCheckpoint(segment.GetInsertChannel(), &msgpb.MsgPosition{MsgID: []byte{0}}) s.NoError(err) } s.checker.checkImportingJob(job) for _, t := range importTasks { task := s.imeta.GetTask(t.GetTaskID()) for _, id := range task.(*importTask).GetSegmentIDs() { segment := s.checker.meta.GetSegment(id) s.Equal(false, segment.GetIsImporting()) } } s.Equal(internalpb.ImportJobState_Completed, s.imeta.GetJob(job.GetJobID()).GetState()) } func (s *ImportCheckerSuite) TestCheckJob_Failed() { mockErr := errors.New("mock err") job := s.imeta.GetJob(s.jobID) // test checkPendingJob alloc := s.checker.alloc.(*NMockAllocator) alloc.EXPECT().allocN(mock.Anything).Return(0, 0, nil) catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(mockErr) s.checker.checkPendingJob(job) preimportTasks := s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(PreImportTaskType)) s.Equal(0, len(preimportTasks)) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(job.GetJobID()).GetState()) alloc.ExpectedCalls = nil alloc.EXPECT().allocN(mock.Anything).Return(0, 0, mockErr) s.checker.checkPendingJob(job) preimportTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(PreImportTaskType)) s.Equal(0, len(preimportTasks)) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(job.GetJobID()).GetState()) alloc.ExpectedCalls = nil alloc.EXPECT().allocN(mock.Anything).Return(0, 0, nil) catalog.ExpectedCalls = nil catalog.EXPECT().SaveImportJob(mock.Anything).Return(nil) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) s.checker.checkPendingJob(job) preimportTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(PreImportTaskType)) s.Equal(2, len(preimportTasks)) s.Equal(internalpb.ImportJobState_PreImporting, s.imeta.GetJob(job.GetJobID()).GetState()) // test checkPreImportingJob for _, t := range preimportTasks { err := s.imeta.UpdateTask(t.GetTaskID(), UpdateState(datapb.ImportTaskStateV2_Completed)) s.NoError(err) } catalog.ExpectedCalls = nil catalog.EXPECT().SaveImportTask(mock.Anything).Return(mockErr) s.checker.checkPreImportingJob(job) importTasks := s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(ImportTaskType)) s.Equal(0, len(importTasks)) s.Equal(internalpb.ImportJobState_PreImporting, s.imeta.GetJob(job.GetJobID()).GetState()) alloc.ExpectedCalls = nil alloc.EXPECT().allocN(mock.Anything).Return(0, 0, mockErr) importTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(ImportTaskType)) s.Equal(0, len(importTasks)) s.Equal(internalpb.ImportJobState_PreImporting, s.imeta.GetJob(job.GetJobID()).GetState()) catalog.ExpectedCalls = nil catalog.EXPECT().SaveImportJob(mock.Anything).Return(nil) catalog.EXPECT().SaveImportTask(mock.Anything).Return(nil) alloc.ExpectedCalls = nil alloc.EXPECT().allocN(mock.Anything).Return(0, 0, nil) s.checker.checkPreImportingJob(job) importTasks = s.imeta.GetTaskBy(WithJob(job.GetJobID()), WithType(ImportTaskType)) s.Equal(1, len(importTasks)) s.Equal(internalpb.ImportJobState_Importing, s.imeta.GetJob(job.GetJobID()).GetState()) } func (s *ImportCheckerSuite) TestCheckTimeout() { catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) var task ImportTask = &preImportTask{ PreImportTask: &datapb.PreImportTask{ JobID: s.jobID, TaskID: 1, State: datapb.ImportTaskStateV2_InProgress, }, } err := s.imeta.AddTask(task) s.NoError(err) s.checker.tryTimeoutJob(s.imeta.GetJob(s.jobID)) job := s.imeta.GetJob(s.jobID) s.Equal(internalpb.ImportJobState_Failed, job.GetState()) s.Equal("import timeout", job.GetReason()) } func (s *ImportCheckerSuite) TestCheckFailure() { catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) pit1 := &preImportTask{ PreImportTask: &datapb.PreImportTask{ JobID: s.jobID, TaskID: 1, State: datapb.ImportTaskStateV2_Pending, }, } err := s.imeta.AddTask(pit1) s.NoError(err) pit2 := &preImportTask{ PreImportTask: &datapb.PreImportTask{ JobID: s.jobID, TaskID: 2, State: datapb.ImportTaskStateV2_Completed, }, } err = s.imeta.AddTask(pit2) s.NoError(err) catalog.ExpectedCalls = nil catalog.EXPECT().SavePreImportTask(mock.Anything).Return(errors.New("mock error")) s.checker.tryFailingTasks(s.imeta.GetJob(s.jobID)) tasks := s.imeta.GetTaskBy(WithJob(s.jobID), WithStates(datapb.ImportTaskStateV2_Failed)) s.Equal(0, len(tasks)) catalog.ExpectedCalls = nil catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) s.checker.tryFailingTasks(s.imeta.GetJob(s.jobID)) tasks = s.imeta.GetTaskBy(WithJob(s.jobID), WithStates(datapb.ImportTaskStateV2_Failed)) s.Equal(2, len(tasks)) } func (s *ImportCheckerSuite) TestCheckGC() { mockErr := errors.New("mock err") catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SaveImportTask(mock.Anything).Return(nil) var task ImportTask = &importTask{ ImportTaskV2: &datapb.ImportTaskV2{ JobID: s.jobID, TaskID: 1, State: datapb.ImportTaskStateV2_Failed, SegmentIDs: []int64{2}, }, } err := s.imeta.AddTask(task) s.NoError(err) // not failed or completed s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(1, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) catalog.EXPECT().SaveImportJob(mock.Anything).Return(nil) err = s.imeta.UpdateJob(s.jobID, UpdateJobState(internalpb.ImportJobState_Failed)) s.NoError(err) // not reach cleanup ts s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(1, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) GCRetention := Params.DataCoordCfg.ImportTaskRetention.GetAsDuration(time.Second) job := s.imeta.GetJob(s.jobID) job.(*importJob).CleanupTs = tsoutil.AddPhysicalDurationOnTs(job.GetCleanupTs(), GCRetention*-2) err = s.imeta.AddJob(job) s.NoError(err) // segment not dropped s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(1, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) err = s.imeta.UpdateTask(task.GetTaskID(), UpdateSegmentIDs([]int64{})) s.NoError(err) // task is not dropped s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(1, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) err = s.imeta.UpdateTask(task.GetTaskID(), UpdateNodeID(NullNodeID)) s.NoError(err) // remove task failed catalog.EXPECT().DropImportTask(mock.Anything).Return(mockErr) s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(1, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) // remove job failed catalog.ExpectedCalls = nil catalog.EXPECT().DropImportTask(mock.Anything).Return(nil) catalog.EXPECT().DropImportJob(mock.Anything).Return(mockErr) s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(0, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(1, len(s.imeta.GetJobBy())) // normal case catalog.ExpectedCalls = nil catalog.EXPECT().DropImportJob(mock.Anything).Return(nil) s.checker.checkGC(s.imeta.GetJob(s.jobID)) s.Equal(0, len(s.imeta.GetTaskBy(WithJob(s.jobID)))) s.Equal(0, len(s.imeta.GetJobBy())) } func (s *ImportCheckerSuite) TestCheckCollection() { mockErr := errors.New("mock err") catalog := s.imeta.(*importMeta).catalog.(*mocks.DataCoordCatalog) catalog.EXPECT().SavePreImportTask(mock.Anything).Return(nil) var task ImportTask = &preImportTask{ PreImportTask: &datapb.PreImportTask{ JobID: s.jobID, TaskID: 1, State: datapb.ImportTaskStateV2_Pending, }, } err := s.imeta.AddTask(task) s.NoError(err) // no jobs s.checker.checkCollection(1, []ImportJob{}) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(s.jobID).GetState()) // collection exist broker := s.checker.broker.(*broker2.MockBroker) broker.EXPECT().HasCollection(mock.Anything, mock.Anything).Return(true, nil) s.checker.checkCollection(1, []ImportJob{s.imeta.GetJob(s.jobID)}) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(s.jobID).GetState()) // HasCollection failed s.checker.broker = broker2.NewMockBroker(s.T()) broker = s.checker.broker.(*broker2.MockBroker) broker.EXPECT().HasCollection(mock.Anything, mock.Anything).Return(true, mockErr) s.checker.checkCollection(1, []ImportJob{s.imeta.GetJob(s.jobID)}) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(s.jobID).GetState()) // SaveImportJob failed s.checker.broker = broker2.NewMockBroker(s.T()) broker = s.checker.broker.(*broker2.MockBroker) broker.EXPECT().HasCollection(mock.Anything, mock.Anything).Return(false, nil) catalog.ExpectedCalls = nil catalog.EXPECT().SaveImportJob(mock.Anything).Return(mockErr) s.checker.checkCollection(1, []ImportJob{s.imeta.GetJob(s.jobID)}) s.Equal(internalpb.ImportJobState_Pending, s.imeta.GetJob(s.jobID).GetState()) // collection dropped s.checker.broker = broker2.NewMockBroker(s.T()) broker = s.checker.broker.(*broker2.MockBroker) broker.EXPECT().HasCollection(mock.Anything, mock.Anything).Return(false, nil) catalog.ExpectedCalls = nil catalog.EXPECT().SaveImportJob(mock.Anything).Return(nil) s.checker.checkCollection(1, []ImportJob{s.imeta.GetJob(s.jobID)}) s.Equal(internalpb.ImportJobState_Failed, s.imeta.GetJob(s.jobID).GetState()) } func TestImportChecker(t *testing.T) { suite.Run(t, new(ImportCheckerSuite)) }