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
1468 lines
44 KiB
Go
1468 lines
44 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 task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/hashicorp/golang-lru/v2/expirable"
|
|
"github.com/samber/lo"
|
|
"go.uber.org/atomic"
|
|
"golang.org/x/time/rate"
|
|
|
|
"github.com/milvus-io/milvus/internal/json"
|
|
"github.com/milvus-io/milvus/internal/querycoordv2/meta"
|
|
"github.com/milvus-io/milvus/internal/querycoordv2/session"
|
|
"github.com/milvus-io/milvus/internal/querycoordv2/utils"
|
|
"github.com/milvus-io/milvus/pkg/v3/metrics"
|
|
"github.com/milvus-io/milvus/pkg/v3/mlog"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/querypb"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/hardware"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/lock"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/timerecord"
|
|
. "github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
const (
|
|
TaskTypeGrow Type = iota + 1
|
|
TaskTypeReduce
|
|
TaskTypeMove
|
|
TaskTypeUpdate
|
|
TaskTypeStatsUpdate
|
|
TaskTypeDropIndex
|
|
)
|
|
|
|
var TaskTypeName = map[Type]string{
|
|
TaskTypeGrow: "Grow",
|
|
TaskTypeReduce: "Reduce",
|
|
TaskTypeMove: "Move",
|
|
TaskTypeUpdate: "Update",
|
|
TaskTypeStatsUpdate: "StatsUpdate",
|
|
}
|
|
|
|
type Type int32
|
|
|
|
func (t Type) String() string {
|
|
return TaskTypeName[t]
|
|
}
|
|
|
|
type replicaSegmentIndex struct {
|
|
ReplicaID int64
|
|
SegmentID int64
|
|
IsGrowing bool
|
|
}
|
|
|
|
func NewReplicaSegmentIndex(task *SegmentTask) replicaSegmentIndex {
|
|
isGrowing := task.Actions()[0].(*SegmentAction).GetScope() == querypb.DataScope_Streaming
|
|
return replicaSegmentIndex{
|
|
ReplicaID: task.ReplicaID(),
|
|
SegmentID: task.SegmentID(),
|
|
IsGrowing: isGrowing,
|
|
}
|
|
}
|
|
|
|
func NewReplicaLeaderIndex(task *LeaderTask) replicaSegmentIndex {
|
|
return replicaSegmentIndex{
|
|
ReplicaID: task.ReplicaID(),
|
|
SegmentID: task.SegmentID(),
|
|
IsGrowing: false,
|
|
}
|
|
}
|
|
|
|
func NewReplicaDropIndex(task *DropIndexTask) replicaSegmentIndex {
|
|
return replicaSegmentIndex{
|
|
ReplicaID: task.ReplicaID(),
|
|
SegmentID: task.SegmentID(),
|
|
IsGrowing: false,
|
|
}
|
|
}
|
|
|
|
type replicaChannelIndex struct {
|
|
ReplicaID int64
|
|
Channel string
|
|
}
|
|
|
|
type taskQueue struct {
|
|
mu sync.RWMutex
|
|
// TaskPriority -> TaskID -> Task
|
|
buckets []map[int64]Task
|
|
}
|
|
|
|
func newTaskQueue() *taskQueue {
|
|
buckets := make([]map[int64]Task, len(TaskPriorities))
|
|
for i := range buckets {
|
|
buckets[i] = make(map[int64]Task)
|
|
}
|
|
return &taskQueue{
|
|
buckets: buckets,
|
|
}
|
|
}
|
|
|
|
func (queue *taskQueue) Len() int {
|
|
queue.mu.RLock()
|
|
defer queue.mu.RUnlock()
|
|
taskNum := 0
|
|
for _, tasks := range queue.buckets {
|
|
taskNum += len(tasks)
|
|
}
|
|
return taskNum
|
|
}
|
|
|
|
func (queue *taskQueue) Add(task Task) {
|
|
queue.mu.Lock()
|
|
defer queue.mu.Unlock()
|
|
bucket := queue.buckets[task.Priority()]
|
|
bucket[task.ID()] = task
|
|
}
|
|
|
|
func (queue *taskQueue) Remove(task Task) {
|
|
queue.mu.Lock()
|
|
defer queue.mu.Unlock()
|
|
bucket := queue.buckets[task.Priority()]
|
|
delete(bucket, task.ID())
|
|
}
|
|
|
|
// Range iterates all tasks in the queue ordered by priority from high to low
|
|
func (queue *taskQueue) Range(fn func(task Task) bool) {
|
|
queue.mu.RLock()
|
|
defer queue.mu.RUnlock()
|
|
for priority := len(queue.buckets) - 1; priority >= 0; priority-- {
|
|
for _, task := range queue.buckets[priority] {
|
|
if !fn(task) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type nodeTaskQueue struct {
|
|
mu sync.RWMutex
|
|
// NodeID -> TaskPriority -> TaskID -> Task
|
|
buckets map[int64][]map[int64]Task
|
|
}
|
|
|
|
func newNodeTaskQueue() *nodeTaskQueue {
|
|
return &nodeTaskQueue{
|
|
buckets: make(map[int64][]map[int64]Task),
|
|
}
|
|
}
|
|
|
|
// Len returns the total number of unique tasks across all nodes.
|
|
// A task appearing in multiple node buckets (e.g. Move task) is counted once.
|
|
func (queue *nodeTaskQueue) Len() int {
|
|
queue.mu.RLock()
|
|
defer queue.mu.RUnlock()
|
|
seen := make(map[int64]struct{})
|
|
for _, nodeBuckets := range queue.buckets {
|
|
for _, bucket := range nodeBuckets {
|
|
for taskID := range bucket {
|
|
seen[taskID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return len(seen)
|
|
}
|
|
|
|
// LenByNode returns the number of tasks related to the specified node.
|
|
func (queue *nodeTaskQueue) LenByNode(nodeID int64) int {
|
|
queue.mu.RLock()
|
|
defer queue.mu.RUnlock()
|
|
nodeBuckets, ok := queue.buckets[nodeID]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
n := 0
|
|
for _, bucket := range nodeBuckets {
|
|
n += len(bucket)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (queue *nodeTaskQueue) Add(task Task) {
|
|
queue.mu.Lock()
|
|
defer queue.mu.Unlock()
|
|
for _, action := range task.Actions() {
|
|
queue.addToBucket(action.Node(), task)
|
|
if la, ok := action.(*LeaderAction); ok {
|
|
queue.addToBucket(la.GetLeaderID(), task)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (queue *nodeTaskQueue) Remove(task Task) {
|
|
queue.mu.Lock()
|
|
defer queue.mu.Unlock()
|
|
for _, action := range task.Actions() {
|
|
queue.removeFromBucket(action.Node(), task)
|
|
if la, ok := action.(*LeaderAction); ok {
|
|
queue.removeFromBucket(la.GetLeaderID(), task)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (queue *nodeTaskQueue) addToBucket(nodeID int64, task Task) {
|
|
nodeBuckets, ok := queue.buckets[nodeID]
|
|
if !ok {
|
|
nodeBuckets = make([]map[int64]Task, len(TaskPriorities))
|
|
for i := range nodeBuckets {
|
|
nodeBuckets[i] = make(map[int64]Task)
|
|
}
|
|
queue.buckets[nodeID] = nodeBuckets
|
|
}
|
|
nodeBuckets[task.Priority()][task.ID()] = task
|
|
}
|
|
|
|
func (queue *nodeTaskQueue) removeFromBucket(nodeID int64, task Task) {
|
|
if nodeBuckets, ok := queue.buckets[nodeID]; ok {
|
|
delete(nodeBuckets[task.Priority()], task.ID())
|
|
}
|
|
}
|
|
|
|
// RangeByNode iterates tasks related to the specified node,
|
|
// ordered by priority from high to low.
|
|
func (queue *nodeTaskQueue) RangeByNode(nodeID int64, fn func(task Task) bool) {
|
|
queue.mu.RLock()
|
|
defer queue.mu.RUnlock()
|
|
nodeBuckets, ok := queue.buckets[nodeID]
|
|
if !ok {
|
|
return
|
|
}
|
|
for priority := len(nodeBuckets) - 1; priority >= 0; priority-- {
|
|
for _, task := range nodeBuckets[priority] {
|
|
if !fn(task) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type SegmentTaskDelta struct {
|
|
mu sync.RWMutex
|
|
// taskID -> segment delta records generated by the task's segment actions.
|
|
records map[int64][]segmentDeltaRecord
|
|
}
|
|
|
|
type segmentDeltaRecord struct {
|
|
collectionID int64
|
|
nodeID int64
|
|
segmentID int64
|
|
channelName string
|
|
scope querypb.DataScope
|
|
actionType ActionType
|
|
delta int
|
|
}
|
|
|
|
type ChannelTaskDelta struct {
|
|
data map[int64]map[int64]int // nodeID -> collectionID -> taskDelta
|
|
nodeTotalDelta map[int64]int // nodeID -> totalTaskDelta
|
|
mu sync.RWMutex // Mutex to protect the map
|
|
|
|
taskIDRecords UniqueSet
|
|
}
|
|
|
|
type SegmentTaskDeltaSnapshot struct {
|
|
nodeDeltas map[int64]int
|
|
nodeCollectionDeltas map[int64]int
|
|
}
|
|
|
|
func NewSegmentTaskDeltaSnapshot(nodeDeltas, nodeCollectionDeltas map[int64]int) *SegmentTaskDeltaSnapshot {
|
|
if nodeDeltas == nil {
|
|
nodeDeltas = make(map[int64]int)
|
|
}
|
|
if nodeCollectionDeltas == nil {
|
|
nodeCollectionDeltas = make(map[int64]int)
|
|
}
|
|
return &SegmentTaskDeltaSnapshot{
|
|
nodeDeltas: nodeDeltas,
|
|
nodeCollectionDeltas: nodeCollectionDeltas,
|
|
}
|
|
}
|
|
|
|
func (snapshot *SegmentTaskDeltaSnapshot) GetByNode(nodeID int64) int {
|
|
if snapshot == nil {
|
|
return 0
|
|
}
|
|
return snapshot.nodeDeltas[nodeID]
|
|
}
|
|
|
|
// GetByNodeInCollection returns the pending segment-task delta on nodeID,
|
|
// scoped to the collection used when the snapshot was built.
|
|
func (snapshot *SegmentTaskDeltaSnapshot) GetByNodeInCollection(nodeID int64) int {
|
|
if snapshot == nil {
|
|
return 0
|
|
}
|
|
return snapshot.nodeCollectionDeltas[nodeID]
|
|
}
|
|
|
|
func NewSegmentTaskDelta() *SegmentTaskDelta {
|
|
return &SegmentTaskDelta{
|
|
records: make(map[int64][]segmentDeltaRecord),
|
|
}
|
|
}
|
|
|
|
func NewChannelTaskDelta() *ChannelTaskDelta {
|
|
return &ChannelTaskDelta{
|
|
data: make(map[int64]map[int64]int),
|
|
nodeTotalDelta: make(map[int64]int),
|
|
taskIDRecords: NewUniqueSet(),
|
|
}
|
|
}
|
|
|
|
// Add updates the segment task delta records.
|
|
func (delta *SegmentTaskDelta) Add(task *SegmentTask) {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
|
|
if _, ok := delta.records[task.ID()]; ok {
|
|
mlog.Warn(task.Context(), "segment task already exists in delta cache",
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.Int64("taskID", task.ID()))
|
|
return
|
|
}
|
|
|
|
records := make([]segmentDeltaRecord, 0, len(task.Actions()))
|
|
for _, action := range task.Actions() {
|
|
segmentAction, ok := action.(*SegmentAction)
|
|
if !ok {
|
|
continue
|
|
}
|
|
records = append(records, segmentDeltaRecord{
|
|
collectionID: task.CollectionID(),
|
|
nodeID: segmentAction.Node(),
|
|
segmentID: segmentAction.GetSegmentID(),
|
|
channelName: segmentAction.GetShard(),
|
|
scope: segmentAction.GetScope(),
|
|
actionType: segmentAction.Type(),
|
|
delta: segmentAction.WorkLoadEffect(),
|
|
})
|
|
}
|
|
delta.records[task.ID()] = records
|
|
}
|
|
|
|
// Sub removes the segment task delta records.
|
|
func (delta *SegmentTaskDelta) Sub(task *SegmentTask) {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
|
|
if _, ok := delta.records[task.ID()]; !ok {
|
|
mlog.Warn(task.Context(), "segment task does not exist in delta cache",
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.Int64("taskID", task.ID()))
|
|
return
|
|
}
|
|
delete(delta.records, task.ID())
|
|
}
|
|
|
|
// GetSegmentSnapshot builds a snapshot for nodeIDs. collectionID binds the scope
|
|
// used by SegmentTaskDeltaSnapshot.GetByNodeInCollection; -1 means all collections.
|
|
func (delta *SegmentTaskDelta) GetSegmentSnapshot(nodeIDs []int64, collectionID int64, distMgr *meta.DistributionManager) *SegmentTaskDeltaSnapshot {
|
|
snapshot := NewSegmentTaskDeltaSnapshot(make(map[int64]int, len(nodeIDs)), make(map[int64]int, len(nodeIDs)))
|
|
nodeSet := NewUniqueSet(nodeIDs...)
|
|
|
|
records := make([]segmentDeltaRecord, 0)
|
|
delta.mu.RLock()
|
|
for _, taskRecords := range delta.records {
|
|
for _, record := range taskRecords {
|
|
if !nodeSet.Contain(record.nodeID) {
|
|
continue
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
}
|
|
delta.mu.RUnlock()
|
|
|
|
for _, record := range records {
|
|
if distMgr != nil && record.isSegmentDistMatched(distMgr) {
|
|
continue
|
|
}
|
|
snapshot.nodeDeltas[record.nodeID] += record.delta
|
|
if collectionID == -1 || record.collectionID == collectionID {
|
|
snapshot.nodeCollectionDeltas[record.nodeID] += record.delta
|
|
}
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func (record segmentDeltaRecord) isSegmentDistMatched(distMgr *meta.DistributionManager) bool {
|
|
if record.segmentID == 0 {
|
|
return false
|
|
}
|
|
|
|
inDist := segmentInDist(distMgr, record.nodeID, record.channelName, record.segmentID, record.scope)
|
|
switch record.actionType {
|
|
case ActionTypeGrow:
|
|
return inDist
|
|
case ActionTypeReduce:
|
|
return !inDist
|
|
default:
|
|
// Only grow/reduce actions affect segment presence; other segment
|
|
// actions have zero workload effect and are treated as reflected.
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (delta *SegmentTaskDelta) printDetailInfos() {
|
|
delta.mu.RLock()
|
|
defer delta.mu.RUnlock()
|
|
|
|
if len(delta.records) > 0 {
|
|
mlog.Info(context.TODO(), "segment task delta cache info",
|
|
mlog.String("records", fmt.Sprintf("%+v", delta.records)),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (delta *SegmentTaskDelta) Clear() {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
delta.records = make(map[int64][]segmentDeltaRecord)
|
|
}
|
|
|
|
// Add updates the channel task delta for the given nodeID and collectionID.
|
|
func (delta *ChannelTaskDelta) Add(task *ChannelTask) {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
|
|
if delta.taskIDRecords.Contain(task.ID()) {
|
|
mlog.Warn(task.Context(), "channel task already exists in delta cache",
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.Int64("taskID", task.ID()))
|
|
return
|
|
}
|
|
delta.taskIDRecords.Insert(task.ID())
|
|
|
|
collectionID := task.CollectionID()
|
|
for _, action := range task.Actions() {
|
|
nodeID := action.Node()
|
|
effect := action.WorkLoadEffect()
|
|
if _, exists := delta.data[nodeID]; !exists {
|
|
delta.data[nodeID] = make(map[int64]int)
|
|
}
|
|
delta.data[nodeID][collectionID] += effect
|
|
delta.nodeTotalDelta[nodeID] += effect
|
|
}
|
|
}
|
|
|
|
// Sub updates the channel task delta for the given nodeID and collectionID by subtracting delta.
|
|
func (delta *ChannelTaskDelta) Sub(task *ChannelTask) {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
|
|
if !delta.taskIDRecords.Contain(task.ID()) {
|
|
mlog.Warn(task.Context(), "channel task does not exist in delta cache",
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.Int64("taskID", task.ID()))
|
|
return
|
|
}
|
|
delta.taskIDRecords.Remove(task.ID())
|
|
|
|
collectionID := task.CollectionID()
|
|
for _, action := range task.Actions() {
|
|
nodeID := action.Node()
|
|
effect := action.WorkLoadEffect()
|
|
if _, exists := delta.data[nodeID]; !exists {
|
|
delta.data[nodeID] = make(map[int64]int)
|
|
}
|
|
|
|
delta.data[nodeID][collectionID] -= effect
|
|
delta.nodeTotalDelta[nodeID] -= effect
|
|
}
|
|
}
|
|
|
|
// Get retrieves the sum of channel task delta for the given nodeID and collectionID.
|
|
// If nodeID or collectionID is -1, it matches all.
|
|
func (delta *ChannelTaskDelta) Get(nodeID, collectionID int64) int {
|
|
delta.mu.RLock()
|
|
defer delta.mu.RUnlock()
|
|
|
|
if nodeID != -1 && collectionID != -1 {
|
|
nodeData, ok := delta.data[nodeID]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return nodeData[collectionID]
|
|
}
|
|
|
|
if nodeID != -1 {
|
|
return delta.nodeTotalDelta[nodeID]
|
|
}
|
|
|
|
var sum int
|
|
if collectionID != -1 {
|
|
for _, collections := range delta.data {
|
|
sum += collections[collectionID]
|
|
}
|
|
return sum
|
|
}
|
|
|
|
for _, effect := range delta.nodeTotalDelta {
|
|
sum += effect
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func (delta *ChannelTaskDelta) printDetailInfos() {
|
|
delta.mu.RLock()
|
|
defer delta.mu.RUnlock()
|
|
|
|
if delta.taskIDRecords.Len() > 0 {
|
|
mlog.Info(context.TODO(), "channel task delta cache info",
|
|
mlog.Any("taskIDRecords", delta.taskIDRecords.Collect()),
|
|
mlog.Any("data", delta.data),
|
|
mlog.Any("nodeTotalDelta", delta.nodeTotalDelta),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (delta *ChannelTaskDelta) Clear() {
|
|
delta.mu.Lock()
|
|
defer delta.mu.Unlock()
|
|
delta.data = make(map[int64]map[int64]int)
|
|
delta.nodeTotalDelta = make(map[int64]int)
|
|
delta.taskIDRecords.Clear()
|
|
}
|
|
|
|
type Scheduler interface {
|
|
Start()
|
|
Stop()
|
|
AddExecutor(nodeID int64)
|
|
RemoveExecutor(nodeID int64)
|
|
Add(task Task) error
|
|
Dispatch(node int64)
|
|
RemoveByNode(node int64)
|
|
GetChannelTaskNum(filters ...TaskFilter) int
|
|
GetSegmentTaskNum(filters ...TaskFilter) int
|
|
GetTasksJSON() string
|
|
|
|
// GetSegmentTaskDeltaSnapshot returns pending segment-task deltas for nodeIDs.
|
|
// collectionID == -1 means the collection-scoped snapshot covers all collections.
|
|
GetSegmentTaskDeltaSnapshot(nodeIDs []int64, collectionID int64) *SegmentTaskDeltaSnapshot
|
|
GetChannelTaskDelta(nodeID int64, collectionID int64) int
|
|
}
|
|
|
|
type taskScheduler struct {
|
|
ctx context.Context
|
|
executors *ConcurrentMap[int64, *Executor] // NodeID -> Executor
|
|
idAllocator func() UniqueID
|
|
|
|
distMgr *meta.DistributionManager
|
|
meta *meta.Meta
|
|
targetMgr meta.TargetManagerInterface
|
|
broker meta.Broker
|
|
cluster session.Cluster
|
|
nodeMgr *session.NodeManager
|
|
|
|
scheduleMu sync.Mutex // guards schedule() and RemoveByNode()
|
|
collKeyLock *lock.KeyLock[int64] // guards Add()
|
|
tasks *ConcurrentMap[UniqueID, struct{}]
|
|
segmentTasks *ConcurrentMap[replicaSegmentIndex, Task]
|
|
channelTasks *ConcurrentMap[replicaChannelIndex, Task]
|
|
processQueue *nodeTaskQueue
|
|
waitQueue *taskQueue
|
|
|
|
taskStats *expirable.LRU[UniqueID, Task]
|
|
lastUpdateMetricTime atomic.Time
|
|
|
|
segmentTaskDelta *SegmentTaskDelta
|
|
channelTaskDelta *ChannelTaskDelta
|
|
}
|
|
|
|
func NewScheduler(ctx context.Context,
|
|
meta *meta.Meta,
|
|
distMgr *meta.DistributionManager,
|
|
targetMgr meta.TargetManagerInterface,
|
|
broker meta.Broker,
|
|
cluster session.Cluster,
|
|
nodeMgr *session.NodeManager,
|
|
) *taskScheduler {
|
|
id := atomic.NewInt64(time.Now().UnixMilli())
|
|
return &taskScheduler{
|
|
ctx: ctx,
|
|
executors: NewConcurrentMap[int64, *Executor](),
|
|
idAllocator: func() UniqueID {
|
|
return id.Inc()
|
|
},
|
|
|
|
distMgr: distMgr,
|
|
meta: meta,
|
|
targetMgr: targetMgr,
|
|
broker: broker,
|
|
cluster: cluster,
|
|
nodeMgr: nodeMgr,
|
|
|
|
collKeyLock: lock.NewKeyLock[int64](),
|
|
tasks: NewConcurrentMap[UniqueID, struct{}](),
|
|
segmentTasks: NewConcurrentMap[replicaSegmentIndex, Task](),
|
|
channelTasks: NewConcurrentMap[replicaChannelIndex, Task](),
|
|
processQueue: newNodeTaskQueue(),
|
|
waitQueue: newTaskQueue(),
|
|
taskStats: expirable.NewLRU[UniqueID, Task](256, nil, time.Minute*15),
|
|
segmentTaskDelta: NewSegmentTaskDelta(),
|
|
channelTaskDelta: NewChannelTaskDelta(),
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) Start() {}
|
|
|
|
func (scheduler *taskScheduler) Stop() {
|
|
scheduler.executors.Range(func(nodeID int64, executor *Executor) bool {
|
|
executor.Stop()
|
|
return true
|
|
})
|
|
|
|
scheduler.segmentTasks.Range(func(_ replicaSegmentIndex, task Task) bool {
|
|
scheduler.remove(task)
|
|
return true
|
|
})
|
|
scheduler.channelTasks.Range(func(_ replicaChannelIndex, task Task) bool {
|
|
scheduler.remove(task)
|
|
return true
|
|
})
|
|
}
|
|
|
|
func (scheduler *taskScheduler) AddExecutor(nodeID int64) {
|
|
executor := NewExecutor(nodeID,
|
|
scheduler.meta,
|
|
scheduler.distMgr,
|
|
scheduler.broker,
|
|
scheduler.targetMgr,
|
|
scheduler.cluster,
|
|
scheduler.nodeMgr)
|
|
|
|
if _, exist := scheduler.executors.GetOrInsert(nodeID, executor); exist {
|
|
return
|
|
}
|
|
executor.Start(scheduler.ctx)
|
|
mlog.Info(scheduler.ctx, "add executor for new QueryNode", mlog.Int64("nodeID", nodeID))
|
|
}
|
|
|
|
func (scheduler *taskScheduler) RemoveExecutor(nodeID int64) {
|
|
executor, ok := scheduler.executors.GetAndRemove(nodeID)
|
|
if ok {
|
|
executor.Stop()
|
|
mlog.Info(scheduler.ctx, "remove executor of offline QueryNode", mlog.Int64("nodeID", nodeID))
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) Add(task Task) error {
|
|
scheduler.collKeyLock.Lock(task.CollectionID())
|
|
defer scheduler.collKeyLock.Unlock(task.CollectionID())
|
|
err := scheduler.preAdd(task)
|
|
if err != nil {
|
|
task.Cancel(err)
|
|
return err
|
|
}
|
|
|
|
task.SetID(scheduler.idAllocator())
|
|
scheduler.waitQueue.Add(task)
|
|
scheduler.tasks.Insert(task.ID(), struct{}{})
|
|
scheduler.incExecutingTaskDelta(task)
|
|
switch task := task.(type) {
|
|
case *SegmentTask:
|
|
index := NewReplicaSegmentIndex(task)
|
|
scheduler.segmentTasks.Insert(index, task)
|
|
|
|
case *ChannelTask:
|
|
index := replicaChannelIndex{task.ReplicaID(), task.Channel()}
|
|
scheduler.channelTasks.Insert(index, task)
|
|
|
|
case *LeaderTask:
|
|
index := NewReplicaLeaderIndex(task)
|
|
scheduler.segmentTasks.Insert(index, task)
|
|
}
|
|
|
|
scheduler.taskStats.Add(task.ID(), task)
|
|
scheduler.updateTaskMetrics()
|
|
mlog.Info(task.Context(), "task added", mlog.String("task", task.String()))
|
|
task.RecordStartTs()
|
|
return nil
|
|
}
|
|
|
|
func (scheduler *taskScheduler) updateTaskMetrics() {
|
|
if time.Since(scheduler.lastUpdateMetricTime.Load()) < 30*time.Second {
|
|
return
|
|
}
|
|
segmentGrowNum, segmentReduceNum, segmentUpdateNum, segmentMoveNum := 0, 0, 0, 0
|
|
leaderGrowNum, leaderReduceNum, leaderUpdateNum := 0, 0, 0
|
|
channelGrowNum, channelReduceNum, channelMoveNum := 0, 0, 0
|
|
scheduler.segmentTasks.Range(func(_ replicaSegmentIndex, task Task) bool {
|
|
switch {
|
|
case len(task.Actions()) > 1:
|
|
segmentMoveNum++
|
|
case task.Actions()[0].Type() == ActionTypeGrow:
|
|
if _, ok := task.Actions()[0].(*SegmentAction); ok {
|
|
segmentGrowNum++
|
|
}
|
|
if _, ok := task.Actions()[0].(*LeaderAction); ok {
|
|
leaderGrowNum++
|
|
}
|
|
case task.Actions()[0].Type() == ActionTypeReduce:
|
|
if _, ok := task.Actions()[0].(*SegmentAction); ok {
|
|
segmentReduceNum++
|
|
}
|
|
if _, ok := task.Actions()[0].(*LeaderAction); ok {
|
|
leaderReduceNum++
|
|
}
|
|
case task.Actions()[0].Type() == ActionTypeUpdate:
|
|
if _, ok := task.Actions()[0].(*SegmentAction); ok {
|
|
segmentUpdateNum++
|
|
}
|
|
if _, ok := task.Actions()[0].(*LeaderAction); ok {
|
|
leaderUpdateNum++
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
scheduler.channelTasks.Range(func(_ replicaChannelIndex, task Task) bool {
|
|
taskType := GetTaskType(task)
|
|
switch taskType {
|
|
case TaskTypeGrow:
|
|
channelGrowNum++
|
|
case TaskTypeReduce:
|
|
channelReduceNum++
|
|
case TaskTypeMove:
|
|
channelMoveNum++
|
|
}
|
|
return true
|
|
})
|
|
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.SegmentGrowTaskLabel).Set(float64(segmentGrowNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.SegmentReduceTaskLabel).Set(float64(segmentReduceNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.SegmentMoveTaskLabel).Set(float64(segmentMoveNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.SegmentUpdateTaskLabel).Set(float64(segmentUpdateNum))
|
|
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.LeaderGrowTaskLabel).Set(float64(leaderGrowNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.LeaderReduceTaskLabel).Set(float64(leaderReduceNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.LeaderUpdateTaskLabel).Set(float64(leaderUpdateNum))
|
|
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.ChannelGrowTaskLabel).Set(float64(channelGrowNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.ChannelReduceTaskLabel).Set(float64(channelReduceNum))
|
|
metrics.QueryCoordTaskNum.WithLabelValues(metrics.ChannelMoveTaskLabel).Set(float64(channelMoveNum))
|
|
scheduler.lastUpdateMetricTime.Store(time.Now())
|
|
}
|
|
|
|
// check whether the task is valid to add,
|
|
// must hold lock
|
|
func (scheduler *taskScheduler) preAdd(task Task) error {
|
|
switch task := task.(type) {
|
|
case *SegmentTask:
|
|
index := NewReplicaSegmentIndex(task)
|
|
if old, ok := scheduler.segmentTasks.Get(index); ok {
|
|
if task.Priority() > old.Priority() {
|
|
mlog.Info(scheduler.ctx, "replace old task, the new one with higher priority",
|
|
mlog.Int64("oldID", old.ID()),
|
|
mlog.String("oldPriority", old.Priority().String()),
|
|
mlog.Int64("newID", task.ID()),
|
|
mlog.String("newPriority", task.Priority().String()),
|
|
)
|
|
old.Cancel(merr.WrapErrServiceInternal("replaced with the other one with higher priority"))
|
|
scheduler.remove(old)
|
|
return nil
|
|
}
|
|
|
|
return merr.WrapErrServiceInternal("task with the same segment exists")
|
|
}
|
|
|
|
taskType := GetTaskType(task)
|
|
|
|
if taskType == TaskTypeMove {
|
|
leader := scheduler.getReplicaShardLeader(task.Shard(), task.ReplicaID())
|
|
if leader == nil {
|
|
return merr.WrapErrServiceInternal("segment's delegator leader not found, stop balancing")
|
|
}
|
|
segmentInTargetNode := scheduler.distMgr.SegmentDistManager.GetByFilter(meta.WithNodeID(task.Actions()[1].Node()), meta.WithSegmentID(task.SegmentID()))
|
|
if len(segmentInTargetNode) == 0 {
|
|
return merr.WrapErrServiceInternal("source segment released, stop balancing")
|
|
}
|
|
}
|
|
|
|
case *ChannelTask:
|
|
index := replicaChannelIndex{task.ReplicaID(), task.Channel()}
|
|
if old, ok := scheduler.channelTasks.Get(index); ok {
|
|
if task.Priority() > old.Priority() {
|
|
mlog.Info(scheduler.ctx, "replace old task, the new one with higher priority",
|
|
mlog.Int64("oldID", old.ID()),
|
|
mlog.String("oldPriority", old.Priority().String()),
|
|
mlog.Int64("newID", task.ID()),
|
|
mlog.String("newPriority", task.Priority().String()),
|
|
)
|
|
old.Cancel(merr.WrapErrServiceInternal("replaced with the other one with higher priority"))
|
|
scheduler.remove(old)
|
|
return nil
|
|
}
|
|
|
|
return merr.WrapErrServiceInternal("task with the same channel exists")
|
|
}
|
|
|
|
taskType := GetTaskType(task)
|
|
switch taskType {
|
|
case TaskTypeGrow:
|
|
delegatorList := scheduler.distMgr.ChannelDistManager.GetByFilter(meta.WithChannelName2Channel(task.Channel()))
|
|
nodesWithChannel := lo.Map(delegatorList, func(v *meta.DmChannel, _ int) UniqueID { return v.Node })
|
|
replicaNodeMap := utils.GroupNodesByReplica(task.ctx, scheduler.meta.ReplicaManager, task.CollectionID(), nodesWithChannel)
|
|
if _, ok := replicaNodeMap[task.ReplicaID()]; ok {
|
|
return merr.WrapErrServiceInternal("channel subscribed, it can be only balanced")
|
|
}
|
|
case TaskTypeMove:
|
|
delegatorList := scheduler.distMgr.ChannelDistManager.GetByFilter(meta.WithChannelName2Channel(task.Channel()))
|
|
_, ok := lo.Find(delegatorList, func(v *meta.DmChannel) bool { return v.Node == task.Actions()[1].Node() })
|
|
if !ok {
|
|
return merr.WrapErrServiceInternal("source channel unsubscribed, stop balancing")
|
|
}
|
|
}
|
|
case *LeaderTask:
|
|
index := NewReplicaLeaderIndex(task)
|
|
if old, ok := scheduler.segmentTasks.Get(index); ok {
|
|
if task.Priority() > old.Priority() {
|
|
mlog.Info(scheduler.ctx, "replace old task, the new one with higher priority",
|
|
mlog.Int64("oldID", old.ID()),
|
|
mlog.String("oldPriority", old.Priority().String()),
|
|
mlog.Int64("newID", task.ID()),
|
|
mlog.String("newPriority", task.Priority().String()),
|
|
)
|
|
old.Cancel(merr.WrapErrServiceInternal("replaced with the other one with higher priority"))
|
|
scheduler.remove(old)
|
|
return nil
|
|
}
|
|
|
|
return merr.WrapErrServiceInternal("task with the same segment exists")
|
|
}
|
|
case *DropIndexTask:
|
|
index := NewReplicaDropIndex(task)
|
|
if old, ok := scheduler.segmentTasks.Get(index); ok {
|
|
if task.Priority() > old.Priority() {
|
|
mlog.Info(scheduler.ctx, "replace old task, the new one with higher priority",
|
|
mlog.Int64("oldID", old.ID()),
|
|
mlog.String("oldPriority", old.Priority().String()),
|
|
mlog.Int64("newID", task.ID()),
|
|
mlog.String("newPriority", task.Priority().String()),
|
|
)
|
|
old.Cancel(merr.WrapErrServiceInternal("replaced with the other one with higher priority"))
|
|
scheduler.remove(old)
|
|
return nil
|
|
}
|
|
|
|
return merr.WrapErrServiceInternal("task with the same segment exists")
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("preAdd: forget to process task type: %+v", task))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (scheduler *taskScheduler) getReplicaShardLeader(channelName string, replicaID int64) *meta.DmChannel {
|
|
replica := scheduler.meta.Get(scheduler.ctx, replicaID)
|
|
if replica == nil {
|
|
return nil
|
|
}
|
|
return scheduler.distMgr.ChannelDistManager.GetShardLeader(channelName, replica)
|
|
}
|
|
|
|
func (scheduler *taskScheduler) tryPromoteAll() {
|
|
// Promote waiting tasks
|
|
toPromote := make([]Task, 0, scheduler.waitQueue.Len())
|
|
toRemove := make([]Task, 0)
|
|
scheduler.waitQueue.Range(func(task Task) bool {
|
|
err := scheduler.promote(task)
|
|
if err != nil {
|
|
task.Cancel(err)
|
|
toRemove = append(toRemove, task)
|
|
mlog.Warn(scheduler.ctx, "failed to promote task",
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Err(err),
|
|
)
|
|
} else {
|
|
toPromote = append(toPromote, task)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
for _, task := range toPromote {
|
|
scheduler.waitQueue.Remove(task)
|
|
}
|
|
for _, task := range toRemove {
|
|
scheduler.remove(task)
|
|
}
|
|
|
|
if len(toPromote) > 0 || len(toRemove) > 0 {
|
|
mlog.Debug(scheduler.ctx, "promoted tasks",
|
|
mlog.Int("promotedNum", len(toPromote)),
|
|
mlog.Int("toRemoveNum", len(toRemove)))
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) promote(task Task) error {
|
|
log := mlog.With(
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.String("source", task.Source().String()),
|
|
)
|
|
|
|
if err := scheduler.check(task, true); err != nil {
|
|
log.Info(task.Context(), "failed to promote task", mlog.Err(err))
|
|
return err
|
|
}
|
|
|
|
scheduler.processQueue.Add(task)
|
|
task.SetStatus(TaskStatusStarted)
|
|
return nil
|
|
}
|
|
|
|
func (scheduler *taskScheduler) Dispatch(node int64) {
|
|
select {
|
|
case <-scheduler.ctx.Done():
|
|
mlog.Info(scheduler.ctx, "scheduler stopped")
|
|
|
|
default:
|
|
scheduler.scheduleMu.Lock()
|
|
defer scheduler.scheduleMu.Unlock()
|
|
scheduler.schedule(node)
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) GetSegmentTaskDeltaSnapshot(nodeIDs []int64, collectionID int64) *SegmentTaskDeltaSnapshot {
|
|
return scheduler.segmentTaskDelta.GetSegmentSnapshot(nodeIDs, collectionID, scheduler.distMgr)
|
|
}
|
|
|
|
func (scheduler *taskScheduler) GetChannelTaskDelta(nodeID, collectionID int64) int {
|
|
return scheduler.channelTaskDelta.Get(nodeID, collectionID)
|
|
}
|
|
|
|
func (scheduler *taskScheduler) incExecutingTaskDelta(task Task) {
|
|
switch task := task.(type) {
|
|
case *SegmentTask:
|
|
scheduler.segmentTaskDelta.Add(task)
|
|
case *ChannelTask:
|
|
scheduler.channelTaskDelta.Add(task)
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) decExecutingTaskDelta(task Task) {
|
|
switch task := task.(type) {
|
|
case *SegmentTask:
|
|
scheduler.segmentTaskDelta.Sub(task)
|
|
case *ChannelTask:
|
|
scheduler.channelTaskDelta.Sub(task)
|
|
}
|
|
}
|
|
|
|
type TaskFilter func(task Task) bool
|
|
|
|
func WithCollectionID2TaskFilter(collectionID int64) TaskFilter {
|
|
return func(task Task) bool {
|
|
return task.CollectionID() == collectionID
|
|
}
|
|
}
|
|
|
|
func WithTaskTypeFilter(taskType Type) TaskFilter {
|
|
return func(task Task) bool {
|
|
return GetTaskType(task) == taskType
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) GetChannelTaskNum(filters ...TaskFilter) int {
|
|
if len(filters) == 0 {
|
|
return scheduler.channelTasks.Len()
|
|
}
|
|
|
|
// rewrite this with for loop
|
|
counter := 0
|
|
scheduler.channelTasks.Range(func(_ replicaChannelIndex, task Task) bool {
|
|
allMatch := true
|
|
for _, filter := range filters {
|
|
if !filter(task) {
|
|
allMatch = false
|
|
break
|
|
}
|
|
}
|
|
if allMatch {
|
|
counter++
|
|
}
|
|
return true
|
|
})
|
|
return counter
|
|
}
|
|
|
|
func (scheduler *taskScheduler) GetSegmentTaskNum(filters ...TaskFilter) int {
|
|
if len(filters) == 0 {
|
|
return scheduler.segmentTasks.Len()
|
|
}
|
|
|
|
// rewrite this with for loop
|
|
counter := 0
|
|
scheduler.segmentTasks.Range(func(_ replicaSegmentIndex, task Task) bool {
|
|
allMatch := true
|
|
for _, filter := range filters {
|
|
if !filter(task) {
|
|
allMatch = false
|
|
break
|
|
}
|
|
}
|
|
if allMatch {
|
|
counter++
|
|
}
|
|
return true
|
|
})
|
|
return counter
|
|
}
|
|
|
|
// GetTasksJSON returns the JSON string of all tasks.
|
|
// the task stats object is thread safe and can be accessed without lock
|
|
func (scheduler *taskScheduler) GetTasksJSON() string {
|
|
tasks := scheduler.taskStats.Values()
|
|
ret, err := json.Marshal(tasks)
|
|
if err != nil {
|
|
mlog.Warn(scheduler.ctx, "marshal tasks fail", mlog.Err(err))
|
|
return ""
|
|
}
|
|
return string(ret)
|
|
}
|
|
|
|
// schedule selects some tasks to execute, follow these steps for each started selected tasks:
|
|
// 1. check whether this task is stale, set status to canceled if stale
|
|
// 2. step up the task's actions, set status to succeeded if all actions finished
|
|
// 3. execute the current action of task
|
|
func (scheduler *taskScheduler) schedule(node int64) {
|
|
if scheduler.tasks.Len() == 0 {
|
|
scheduler.updateTaskMetrics()
|
|
return
|
|
}
|
|
|
|
tr := timerecord.NewTimeRecorder("")
|
|
log := mlog.With(
|
|
mlog.Int64("nodeID", node),
|
|
)
|
|
|
|
scheduler.tryPromoteAll()
|
|
promoteDur := tr.RecordSpan()
|
|
|
|
log.Debug(scheduler.ctx, "process tasks related to node",
|
|
mlog.Int("processingTaskNum", scheduler.processQueue.LenByNode(node)),
|
|
mlog.Int("waitingTaskNum", scheduler.waitQueue.Len()),
|
|
mlog.Int("segmentTaskNum", scheduler.segmentTasks.Len()),
|
|
mlog.Int("channelTaskNum", scheduler.channelTasks.Len()),
|
|
)
|
|
|
|
// Process tasks
|
|
toProcess := make([]Task, 0)
|
|
toRemove := make([]Task, 0)
|
|
scheduler.processQueue.RangeByNode(node, func(task Task) bool {
|
|
shouldProcess := scheduler.preProcess(task)
|
|
if shouldProcess {
|
|
toProcess = append(toProcess, task)
|
|
} else if task.Status() != TaskStatusStarted {
|
|
toRemove = append(toRemove, task)
|
|
}
|
|
|
|
return true
|
|
})
|
|
preprocessDur := tr.RecordSpan()
|
|
|
|
// The scheduler doesn't limit the number of tasks,
|
|
// to commit tasks to executors as soon as possible, to reach higher merge possibility
|
|
commmittedNum := atomic.NewInt32(0)
|
|
funcutil.ProcessFuncParallel(len(toProcess), hardware.GetCPUNum(), func(idx int) error {
|
|
if scheduler.process(toProcess[idx]) {
|
|
commmittedNum.Inc()
|
|
}
|
|
return nil
|
|
}, "process")
|
|
processDur := tr.RecordSpan()
|
|
|
|
for _, task := range toRemove {
|
|
scheduler.remove(task)
|
|
}
|
|
|
|
scheduler.updateTaskMetrics()
|
|
|
|
log.Info(scheduler.ctx, "processed tasks",
|
|
mlog.Int("toProcessNum", len(toProcess)),
|
|
mlog.Int32("committedNum", commmittedNum.Load()),
|
|
mlog.Int("toRemoveNum", len(toRemove)),
|
|
mlog.Duration("promoteDur", promoteDur),
|
|
mlog.Duration("preprocessDUr", preprocessDur),
|
|
mlog.Duration("processDUr", processDur),
|
|
mlog.Duration("totalDur", tr.ElapseSpan()),
|
|
)
|
|
|
|
log.Info(scheduler.ctx, "process tasks related to node done",
|
|
mlog.Int("processingTaskNum", scheduler.processQueue.LenByNode(node)),
|
|
mlog.Int("waitingTaskNum", scheduler.waitQueue.Len()),
|
|
mlog.Int("segmentTaskNum", scheduler.segmentTasks.Len()),
|
|
mlog.Int("channelTaskNum", scheduler.channelTasks.Len()),
|
|
)
|
|
}
|
|
|
|
func (scheduler *taskScheduler) isRelated(task Task, node int64) bool {
|
|
for _, action := range task.Actions() {
|
|
// For LeaderAction, the worker node and executor node are both related.
|
|
if action.Node() == node || actionExecutorNode(action) == node {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func actionExecutorNode(action Action) int64 {
|
|
if la, ok := action.(*LeaderAction); ok {
|
|
return la.GetLeaderID()
|
|
}
|
|
return action.Node()
|
|
}
|
|
|
|
// preProcess checks the finished actions of task,
|
|
// and converts the task's status,
|
|
// return true if the task should be executed,
|
|
// false otherwise
|
|
func (scheduler *taskScheduler) preProcess(task Task) (shouldProcess bool) {
|
|
if task.Status() != TaskStatusStarted {
|
|
return false
|
|
}
|
|
|
|
actions, step := task.Actions(), task.Step()
|
|
shouldProcess = true
|
|
for step < len(actions) {
|
|
finished, waitingDist := scheduler.checkActionFinish(actions[step])
|
|
if !finished {
|
|
if waitingDist {
|
|
shouldProcess = false
|
|
}
|
|
break
|
|
}
|
|
if GetTaskType(task) == TaskTypeMove && actions[step].Type() == ActionTypeGrow {
|
|
var newDelegatorReady bool
|
|
switch action := actions[step].(type) {
|
|
case *ChannelAction:
|
|
// wait for new delegator becomes leader, then try to remove old leader
|
|
task := task.(*ChannelTask)
|
|
delegator := scheduler.getReplicaShardLeader(task.Shard(), task.ReplicaID())
|
|
mlog.Debug(scheduler.ctx, "process channelAction", mlog.Bool("delegator is Nil", delegator == nil))
|
|
if delegator != nil {
|
|
mlog.Debug(scheduler.ctx, "process channelAction", mlog.Int64("delegator node", delegator.Node),
|
|
mlog.Int64("action node", action.Node()))
|
|
}
|
|
newDelegatorReady = delegator != nil && delegator.Node == action.Node()
|
|
default:
|
|
newDelegatorReady = true
|
|
}
|
|
if !newDelegatorReady {
|
|
mlog.RatedInfo(scheduler.ctx, rate.Limit(30), "Blocking reduce action in balance channel task",
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.String("channelName", task.Shard()),
|
|
mlog.Int64("taskID", task.ID()))
|
|
|
|
break
|
|
}
|
|
}
|
|
task.StepUp()
|
|
step++
|
|
}
|
|
|
|
if task.IsFinished(scheduler.distMgr) {
|
|
task.SetStatus(TaskStatusSucceeded)
|
|
} else {
|
|
if err := scheduler.check(task, false); err != nil {
|
|
task.Cancel(err)
|
|
}
|
|
}
|
|
|
|
return shouldProcess && task.Status() == TaskStatusStarted
|
|
}
|
|
|
|
func (scheduler *taskScheduler) checkActionFinish(action Action) (finished bool, waitingDist bool) {
|
|
segmentAction, ok := action.(*SegmentAction)
|
|
if !ok {
|
|
return action.IsFinished(scheduler.distMgr), false
|
|
}
|
|
if !segmentAction.rpcReturned.Load() {
|
|
return false, false
|
|
}
|
|
distMatched := segmentAction.isDistMatched(scheduler.distMgr)
|
|
return distMatched, !distMatched
|
|
}
|
|
|
|
// process processes the given task,
|
|
// return true if the task is started and succeeds to commit the current action
|
|
func (scheduler *taskScheduler) process(task Task) bool {
|
|
log := mlog.With(
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.String("type", GetTaskType(task).String()),
|
|
mlog.String("source", task.Source().String()),
|
|
)
|
|
|
|
actions, step := task.Actions(), task.Step()
|
|
nodeID := actionExecutorNode(actions[step])
|
|
executor, ok := scheduler.executors.Get(nodeID)
|
|
if !ok {
|
|
log.Warn(task.Context(), "no executor for QueryNode",
|
|
mlog.Int("step", step),
|
|
mlog.Int64("nodeID", nodeID))
|
|
return false
|
|
}
|
|
|
|
return executor.Execute(task, step)
|
|
}
|
|
|
|
func (scheduler *taskScheduler) check(task Task, checkDistExist bool) error {
|
|
err := task.Context().Err()
|
|
if err == nil {
|
|
err = scheduler.checkStale(task, checkDistExist)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (scheduler *taskScheduler) RemoveByNode(node int64) {
|
|
scheduler.scheduleMu.Lock()
|
|
defer scheduler.scheduleMu.Unlock()
|
|
|
|
scheduler.segmentTasks.Range(func(_ replicaSegmentIndex, task Task) bool {
|
|
if scheduler.isRelated(task, node) {
|
|
scheduler.remove(task)
|
|
}
|
|
return true
|
|
})
|
|
scheduler.channelTasks.Range(func(_ replicaChannelIndex, task Task) bool {
|
|
if scheduler.isRelated(task, node) {
|
|
scheduler.remove(task)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func (scheduler *taskScheduler) recordSegmentTaskError(task *SegmentTask) {
|
|
mlog.Warn(scheduler.ctx, "task scheduler recordSegmentTaskError",
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.Int64("segmentID", task.SegmentID()),
|
|
mlog.String("status", task.Status()),
|
|
mlog.Err(task.err),
|
|
)
|
|
meta.GlobalFailedLoadCache.Put(task.collectionID, task.Err())
|
|
}
|
|
|
|
func (scheduler *taskScheduler) remove(task Task) {
|
|
log := mlog.With(
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.String("status", task.Status()),
|
|
)
|
|
|
|
if errors.Is(task.Err(), merr.ErrSegmentNotFound) {
|
|
log.Info(task.Context(), "segment in target has been cleaned, trigger force update next target")
|
|
// Avoid using task.Ctx as it may be canceled before remove is called.
|
|
scheduler.targetMgr.UpdateCollectionNextTarget(scheduler.ctx, task.CollectionID())
|
|
}
|
|
|
|
// If task failed due to resource exhaustion (OOM, disk full, GPU OOM, etc.),
|
|
// mark the node as resource exhausted for a penalty period.
|
|
// During this period, the balancer will skip this node when assigning new segments/channels.
|
|
// This prevents continuous failures on the same node and allows it time to recover.
|
|
if errors.Is(task.Err(), merr.ErrSegmentRequestResourceFailed) {
|
|
for _, action := range task.Actions() {
|
|
if action.Type() == ActionTypeGrow {
|
|
nodeID := action.Node()
|
|
duration := paramtable.Get().QueryCoordCfg.ResourceExhaustionPenaltyDuration.GetAsDuration(time.Second)
|
|
scheduler.nodeMgr.MarkResourceExhaustion(nodeID, duration)
|
|
log.Info(task.Context(), "mark resource exhaustion for node", mlog.Int64("nodeID", nodeID), mlog.Duration("duration", duration), mlog.Err(task.Err()))
|
|
}
|
|
}
|
|
}
|
|
|
|
task.Cancel(nil)
|
|
_, ok := scheduler.tasks.GetAndRemove(task.ID())
|
|
scheduler.waitQueue.Remove(task)
|
|
scheduler.processQueue.Remove(task)
|
|
if ok {
|
|
scheduler.decExecutingTaskDelta(task)
|
|
|
|
if scheduler.tasks.Len() == 0 {
|
|
// in case of task delta leak, try to print detail info before clear
|
|
scheduler.segmentTaskDelta.printDetailInfos()
|
|
scheduler.segmentTaskDelta.Clear()
|
|
scheduler.channelTaskDelta.printDetailInfos()
|
|
scheduler.channelTaskDelta.Clear()
|
|
}
|
|
}
|
|
|
|
switch task := task.(type) {
|
|
case *SegmentTask:
|
|
index := NewReplicaSegmentIndex(task)
|
|
scheduler.segmentTasks.Remove(index)
|
|
log = mlog.With(mlog.Int64("segmentID", task.SegmentID()))
|
|
if task.Status() == TaskStatusFailed &&
|
|
task.Err() != nil &&
|
|
!errors.IsAny(task.Err(), merr.ErrChannelNotFound, merr.ErrServiceTooManyRequests) {
|
|
scheduler.recordSegmentTaskError(task)
|
|
}
|
|
|
|
case *ChannelTask:
|
|
index := replicaChannelIndex{task.ReplicaID(), task.Channel()}
|
|
scheduler.channelTasks.Remove(index)
|
|
log = mlog.With(mlog.String("channel", task.Channel()))
|
|
|
|
case *LeaderTask:
|
|
index := NewReplicaLeaderIndex(task)
|
|
scheduler.segmentTasks.Remove(index)
|
|
log = mlog.With(mlog.Int64("segmentID", task.SegmentID()))
|
|
}
|
|
|
|
log.Info(task.Context(), "task removed")
|
|
|
|
if scheduler.meta.Exist(task.Context(), task.CollectionID()) {
|
|
metrics.QueryCoordTaskLatency.WithLabelValues(fmt.Sprint(task.CollectionID()),
|
|
scheduler.getTaskMetricsLabel(task), task.Shard()).Observe(float64(task.GetTaskLatency()))
|
|
}
|
|
}
|
|
|
|
func (scheduler *taskScheduler) getTaskMetricsLabel(task Task) string {
|
|
taskType := GetTaskType(task)
|
|
switch task.(type) {
|
|
case *SegmentTask:
|
|
switch taskType {
|
|
case TaskTypeGrow:
|
|
return metrics.SegmentGrowTaskLabel
|
|
case TaskTypeReduce:
|
|
return metrics.SegmentReduceTaskLabel
|
|
case TaskTypeMove:
|
|
return metrics.SegmentMoveTaskLabel
|
|
case TaskTypeUpdate:
|
|
return metrics.SegmentUpdateTaskLabel
|
|
}
|
|
|
|
case *ChannelTask:
|
|
switch taskType {
|
|
case TaskTypeGrow:
|
|
return metrics.ChannelGrowTaskLabel
|
|
case TaskTypeReduce:
|
|
return metrics.ChannelReduceTaskLabel
|
|
case TaskTypeMove:
|
|
return metrics.ChannelMoveTaskLabel
|
|
}
|
|
|
|
case *LeaderTask:
|
|
switch taskType {
|
|
case TaskTypeGrow:
|
|
return metrics.LeaderGrowTaskLabel
|
|
case TaskTypeReduce:
|
|
return metrics.LeaderReduceTaskLabel
|
|
}
|
|
}
|
|
|
|
return metrics.UnknownTaskLabel
|
|
}
|
|
|
|
func WrapTaskLog(task Task, fields ...mlog.Field) []mlog.Field {
|
|
res := []mlog.Field{
|
|
mlog.Int64("taskID", task.ID()),
|
|
mlog.Int64("collectionID", task.CollectionID()),
|
|
mlog.Int64("replicaID", task.ReplicaID()),
|
|
mlog.String("source", task.Source().String()),
|
|
}
|
|
res = append(res, fields...)
|
|
return res
|
|
}
|
|
|
|
func (scheduler *taskScheduler) checkStale(task Task, checkDistExist bool) error {
|
|
// Get replica, but only fail if we need it for RO node check
|
|
// NilReplica (ID=-1) is used for reduce-only tasks like unsubscribe channel
|
|
var replica *meta.Replica
|
|
if task.ReplicaID() != -1 {
|
|
replica = scheduler.meta.Get(scheduler.ctx, task.ReplicaID())
|
|
if replica == nil {
|
|
mlog.Warn(task.Context(), "task stale due to replica not found", mlog.String("task", task.String()))
|
|
return merr.WrapErrReplicaNotFound(task.ReplicaID())
|
|
}
|
|
}
|
|
|
|
// For segment grow tasks, check if segment is already loaded in dist.
|
|
// This prevents duplicate load RPCs when the checker creates tasks from a stale dist snapshot
|
|
// but the segment has already been loaded before the task is dispatched.
|
|
// Only checked during promote (waitQueue → processQueue), not during preProcess,
|
|
// because during preProcess the segment may have been loaded by this task's own in-flight RPC,
|
|
// and canceling the task would kill that RPC with a misleading "context canceled" error.
|
|
if checkDistExist {
|
|
if segmentTask, ok := task.(*SegmentTask); ok && GetTaskType(task) == TaskTypeGrow && replica != nil {
|
|
existsInDist := scheduler.distMgr.SegmentDistManager.GetByFilter(
|
|
meta.WithCollectionID(task.CollectionID()),
|
|
meta.WithReplica(replica),
|
|
meta.WithSegmentID(segmentTask.SegmentID()),
|
|
)
|
|
if len(existsInDist) > 0 {
|
|
mlog.Info(task.Context(), "task stale due to segment already loaded in dist",
|
|
mlog.String("task", task.String()),
|
|
mlog.Int64("segmentID", segmentTask.SegmentID()))
|
|
return merr.WrapErrServiceInternal("segment already loaded in dist")
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, action := range task.Actions() {
|
|
// Determine the target node for stale checking.
|
|
// For LeaderAction, we need to check the leader node (delegator) instead of the worker node.
|
|
// This is because LeaderAction.Node() returns the worker node where the segment resides,
|
|
// but the task is executed on the leader node. If the worker node is an RO node while
|
|
// the leader node is still RW, the task should NOT be marked as stale.
|
|
// See issue #46737: Using action.Node() for LeaderAction incorrectly marks tasks as stale
|
|
// when syncing segments from RO nodes to the delegator, blocking balance channel operations.
|
|
var targetNode int64
|
|
switch a := action.(type) {
|
|
case *LeaderAction:
|
|
targetNode = a.GetLeaderID()
|
|
default:
|
|
targetNode = a.Node()
|
|
}
|
|
|
|
nodeInfo := scheduler.nodeMgr.Get(targetNode)
|
|
if nodeInfo == nil {
|
|
mlog.Warn(task.Context(), "task stale due to node not found", mlog.String("task", task.String()), mlog.Int64("nodeID", targetNode))
|
|
return merr.WrapErrNodeNotFound(targetNode)
|
|
}
|
|
if action.Type() == ActionTypeGrow {
|
|
if nodeInfo.IsStoppingState() {
|
|
mlog.Warn(task.Context(), "task stale due to node offline", mlog.String("task", task.String()), mlog.Int64("nodeID", targetNode))
|
|
return merr.WrapErrNodeOffline(targetNode)
|
|
}
|
|
|
|
if replica != nil && (replica.ContainRONode(targetNode) || replica.ContainROSQNode(targetNode)) {
|
|
mlog.Warn(task.Context(), "task stale due to node becomes ro node", mlog.String("task", task.String()), mlog.Int64("nodeID", targetNode))
|
|
return merr.WrapErrNodeStateUnexpected(targetNode, "node becomes ro node")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|