karmada/pkg/scheduler/core/spreadconstraint/select_groups.go

225 lines
6.3 KiB
Go
Executable File

/*
Copyright 2023 The Karmada Authors.
Licensed 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 spreadconstraint
import (
"fmt"
"sort"
"strings"
"k8s.io/klog/v2"
)
// GroupInfo indicate the group info.
type GroupInfo struct {
name string
value int
weight int64
}
type dfsPath struct {
id int
groups []*GroupInfo
weight int64
value int
}
func (path *dfsPath) next() *dfsPath {
path.id++
result := new(dfsPath)
*result = *path
if path.groups != nil {
result.groups = make([]*GroupInfo, len(path.groups))
copy(result.groups, path.groups)
}
for _, group := range result.groups {
result.weight += group.weight
result.value += group.value
}
result.sortGroups()
return result
}
func (path *dfsPath) enqueue(group *GroupInfo) {
path.groups = append(path.groups, group)
}
func (path *dfsPath) popLast() {
path.groups = path.groups[:path.length()-1]
}
func (path *dfsPath) length() int {
return len(path.groups)
}
func (path *dfsPath) sortGroups() {
sort.Slice(path.groups, func(i, j int) bool {
if path.groups[i].weight != path.groups[j].weight {
return path.groups[i].weight > path.groups[j].weight
}
return path.groups[i].name < path.groups[j].name
})
}
func (path *dfsPath) matchSubPath(subPath *dfsPath) bool {
if subPath.length() >= path.length() {
return false
}
for i, group := range subPath.groups {
if path.groups[i].name != group.name {
return false
}
}
return true
}
func (path *dfsPath) String() string {
var groupNames []string
for _, group := range path.groups {
groupNames = append(groupNames, group.name)
}
return fmt.Sprintf("[%s]", strings.Join(groupNames, "->"))
}
// selectGroups select best groups in all groups.
func selectGroups(groups []*GroupInfo, minConstraint, maxConstraint, target int) []*GroupInfo {
if len(groups) == 0 {
return nil
}
feasiblePaths := findFeasiblePaths(groups, minConstraint, maxConstraint, target)
if len(feasiblePaths) == 0 {
return nil
}
klog.V(4).Infof("find feasible paths: %v", feasiblePaths)
return prioritizePaths(feasiblePaths).groups
}
// findFeasiblePaths find all feasible path based on DFS.
// We give an example of a tree diagram for easy understanding, assuming: target=7, groups: [2,3,6,7].
// Note: groups is only briefly represented by the value.
//
// +---------------------------------+
// | target=7 |
// +---------------+-------------+---+
// / | | \
// 2/ 3| 6| \7
// / | | \
// +---+ +-+-+ +-+-+ +---+
// | 5 | | 4 | | 1 | | 0 |
// +-+-+ +---+ +-+-+ +---+
// / | \ / \ |
// 3/ 6| \7 6/ \7 7|
// / | \ / \ |
// +---+ +-+-+ +---+ +---+ +---+ +-+-+
// | 2 | |-1 | |-2 | |-2 | |-3 | |-6 |
// +---+ +---+ +---+ +---+ +---+ +---+
// / \
// 6/ \7
// / \
// +---+ +---+
// |-4 | |-5 |
// +---+ +---+
//
// The path from the root path to all leaf nodes whose value is less than or equal to 0 is a feasible combination.
// minConstraint and maxConstraint are used to limit the length of the path. Therefore, we can exclude some
// combinations whose path is too long or too short during the search process.
// If we set minConstraint=2 and maxConstraint=2, so the feasible paths is: [2,6] [2,7] [3,6] [3,7] [6,7]
func findFeasiblePaths(groups []*GroupInfo, minConstraint, maxConstraint, target int) (paths []*dfsPath) {
if len(groups) > 1 {
sort.Slice(groups, func(i, j int) bool {
if groups[i].value != groups[j].value {
return groups[i].value < groups[j].value
}
if groups[i].weight != groups[j].weight {
return groups[i].weight > groups[j].weight
}
return groups[i].name < groups[j].name
})
}
rootPath := new(dfsPath)
var dfsFunc func(int, int)
dfsFunc = func(sum, begin int) {
if sum >= target && rootPath.length() >= minConstraint && rootPath.length() <= maxConstraint {
paths = append(paths, rootPath.next())
return
}
// pruning makes DFS faster
if rootPath.length() >= maxConstraint {
return
}
for i := begin; i < len(groups); i++ {
sum += groups[i].value
rootPath.enqueue(groups[i])
dfsFunc(sum, i+1)
// stop backtracking when we have to traverse all groups to satisfy the minimum constraint.
if len(groups) == minConstraint {
break
}
sum -= groups[i].value
rootPath.popLast()
}
}
dfsFunc(0, 0)
return
}
// prioritizePaths select a best path from feasible paths based on a multi-dimensional prioritization strategy.
// That's: weight > value > id(just for predictable result).
// But for the existence of subpaths, we should give priority to subpaths.
// For example:
// G1 -> G2 -> G3
// G1 -> G2
// G1 -> G3
// The best path is [G1 -> G2 -> G3], but [G1 -> G2] is its subpath. So we should choose [G1 -> G2].
func prioritizePaths(feasiblePaths []*dfsPath) *dfsPath {
if len(feasiblePaths) == 1 {
return feasiblePaths[0]
}
sort.Slice(feasiblePaths, func(i, j int) bool {
if feasiblePaths[i].weight != feasiblePaths[j].weight {
return feasiblePaths[i].weight > feasiblePaths[j].weight
}
if feasiblePaths[i].value != feasiblePaths[j].value {
return feasiblePaths[i].value > feasiblePaths[j].value
}
return feasiblePaths[i].id < feasiblePaths[j].id
})
finalPath := feasiblePaths[0]
for i := 1; i < len(feasiblePaths); i++ {
if finalPath.matchSubPath(feasiblePaths[i]) {
finalPath = feasiblePaths[i]
}
}
return finalPath
}