deb-bootc-compose/internal/performance/strategies.go
2025-08-18 23:32:51 -07:00

458 lines
11 KiB
Go

package performance
import (
"fmt"
"math/rand"
"sort"
"sync"
"time"
)
// RoundRobinStrategy implements round-robin load balancing
type RoundRobinStrategy struct {
currentIndex int
mu sync.Mutex
}
// NewRoundRobinStrategy creates a new round-robin strategy
func NewRoundRobinStrategy() *RoundRobinStrategy {
return &RoundRobinStrategy{
currentIndex: 0,
}
}
// SelectNode selects a node using round-robin algorithm
func (rr *RoundRobinStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
rr.mu.Lock()
defer rr.mu.Unlock()
// Convert map to slice for indexing
nodeSlice := make([]*Node, 0, len(nodes))
for _, node := range nodes {
nodeSlice = append(nodeSlice, node)
}
// Sort by ID for consistent ordering
sort.Slice(nodeSlice, func(i, j int) bool {
return nodeSlice[i].ID < nodeSlice[j].ID
})
// Select next node in round-robin fashion
selectedNode := nodeSlice[rr.currentIndex%len(nodeSlice)]
rr.currentIndex++
return selectedNode, nil
}
// GetName returns the strategy name
func (rr *RoundRobinStrategy) GetName() string {
return "round_robin"
}
// LeastConnectionsStrategy implements least connections load balancing
type LeastConnectionsStrategy struct{}
// NewLeastConnectionsStrategy creates a new least connections strategy
func NewLeastConnectionsStrategy() *LeastConnectionsStrategy {
return &LeastConnectionsStrategy{}
}
// SelectNode selects a node with the least active connections
func (lc *LeastConnectionsStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
var selectedNode *Node
minConnections := int(^uint(0) >> 1) // Max int
for _, node := range nodes {
if node.Metrics == nil {
continue
}
activeJobs := node.Metrics.ActiveJobs
if activeJobs < minConnections {
minConnections = activeJobs
selectedNode = node
}
}
if selectedNode == nil {
return nil, fmt.Errorf("no suitable node found")
}
return selectedNode, nil
}
// GetName returns the strategy name
func (lc *LeastConnectionsStrategy) GetName() string {
return "least_connections"
}
// WeightedRoundRobinStrategy implements weighted round-robin load balancing
type WeightedRoundRobinStrategy struct {
currentIndex int
mu sync.Mutex
}
// NewWeightedRoundRobinStrategy creates a new weighted round-robin strategy
func NewWeightedRoundRobinStrategy() *WeightedRoundRobinStrategy {
return &WeightedRoundRobinStrategy{
currentIndex: 0,
}
}
// SelectNode selects a node using weighted round-robin algorithm
func (wr *WeightedRoundRobinStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
wr.mu.Lock()
defer wr.mu.Unlock()
// Convert map to slice and calculate weights
type weightedNode struct {
node *Node
weight int
}
var weightedNodes []weightedNode
for _, node := range nodes {
weight := 1 // Default weight
// Calculate weight based on node capabilities and current load
if node.Metrics != nil {
// Higher weight for nodes with more capacity
availableCapacity := node.Metrics.MaxJobs - node.Metrics.ActiveJobs
if availableCapacity > 0 {
weight = availableCapacity
}
}
// Apply tags-based weight adjustments
if node.Tags != nil {
if priority, ok := node.Tags["priority"]; ok {
switch priority {
case "high":
weight *= 2
case "low":
weight /= 2
}
}
}
weightedNodes = append(weightedNodes, weightedNode{node: node, weight: weight})
}
// Sort by weight (descending)
sort.Slice(weightedNodes, func(i, j int) bool {
return weightedNodes[i].weight > weightedNodes[j].weight
})
// Select next node in weighted round-robin fashion
selectedNode := weightedNodes[wr.currentIndex%len(weightedNodes)].node
wr.currentIndex++
return selectedNode, nil
}
// GetName returns the strategy name
func (wr *WeightedRoundRobinStrategy) GetName() string {
return "weighted_round_robin"
}
// RandomStrategy implements random load balancing
type RandomStrategy struct {
rand *rand.Rand
mu sync.Mutex
}
// NewRandomStrategy creates a new random strategy
func NewRandomStrategy() *RandomStrategy {
return &RandomStrategy{
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
// SelectNode selects a random node
func (r *RandomStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
r.mu.Lock()
defer r.mu.Unlock()
// Convert map to slice
nodeSlice := make([]*Node, 0, len(nodes))
for _, node := range nodes {
nodeSlice = append(nodeSlice, node)
}
// Select random node
randomIndex := r.rand.Intn(len(nodeSlice))
return nodeSlice[randomIndex], nil
}
// GetName returns the strategy name
func (r *RandomStrategy) GetName() string {
return "random"
}
// LeastResponseTimeStrategy implements least response time load balancing
type LeastResponseTimeStrategy struct{}
// NewLeastResponseTimeStrategy creates a new least response time strategy
func NewLeastResponseTimeStrategy() *LeastResponseTimeStrategy {
return &LeastResponseTimeStrategy{}
}
// SelectNode selects a node with the least response time
func (lrt *LeastResponseTimeStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
var selectedNode *Node
minResponseTime := float64(^uint(0) >> 1) // Max float64
for _, node := range nodes {
if node.Metrics == nil {
continue
}
// Use load average as a proxy for response time
// In a real implementation, you'd have actual response time metrics
responseTime := node.Metrics.LoadAverage
if responseTime < minResponseTime {
minResponseTime = responseTime
selectedNode = node
}
}
if selectedNode == nil {
return nil, fmt.Errorf("no suitable node found")
}
return selectedNode, nil
}
// GetName returns the strategy name
func (lrt *LeastResponseTimeStrategy) GetName() string {
return "least_response_time"
}
// IPHashStrategy implements IP hash load balancing
type IPHashStrategy struct{}
// NewIPHashStrategy creates a new IP hash strategy
func NewIPHashStrategy() *IPHashStrategy {
return &IPHashStrategy{}
}
// SelectNode selects a node using IP hash algorithm
func (ih *IPHashStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
// Extract client IP from request metadata
clientIP := "127.0.0.1" // Default IP
if request.Metadata != nil {
if ip, ok := request.Metadata["client_ip"].(string); ok {
clientIP = ip
}
}
// Calculate hash of client IP
hash := hashString(clientIP)
// Convert map to slice for indexing
nodeSlice := make([]*Node, 0, len(nodes))
for _, node := range nodes {
nodeSlice = append(nodeSlice, node)
}
// Sort by ID for consistent ordering
sort.Slice(nodeSlice, func(i, j int) bool {
return nodeSlice[i].ID < nodeSlice[j].ID
})
// Select node based on hash
selectedIndex := hash % uint32(len(nodeSlice))
return nodeSlice[selectedIndex], nil
}
// GetName returns the strategy name
func (ih *IPHashStrategy) GetName() string {
return "ip_hash"
}
// hashString calculates a simple hash of a string
func hashString(s string) uint32 {
var hash uint32
for _, char := range s {
hash = ((hash << 5) + hash) + uint32(char)
}
return hash
}
// AdaptiveStrategy implements adaptive load balancing based on multiple factors
type AdaptiveStrategy struct {
mu sync.Mutex
}
// NewAdaptiveStrategy creates a new adaptive strategy
func NewAdaptiveStrategy() *AdaptiveStrategy {
return &AdaptiveStrategy{}
}
// SelectNode selects a node using adaptive algorithm
func (a *AdaptiveStrategy) SelectNode(nodes map[string]*Node, request *LoadRequest) (*Node, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("no nodes available")
}
a.mu.Lock()
defer a.mu.Unlock()
// Score each node based on multiple factors
type scoredNode struct {
node *Node
score float64
}
var scoredNodes []scoredNode
for _, node := range nodes {
if node.Metrics == nil {
continue
}
score := a.calculateNodeScore(node, request)
scoredNodes = append(scoredNodes, scoredNode{node: node, score: score})
}
if len(scoredNodes) == 0 {
return nil, fmt.Errorf("no suitable node found")
}
// Sort by score (descending)
sort.Slice(scoredNodes, func(i, j int) bool {
return scoredNodes[i].score > scoredNodes[j].score
})
// Return the highest scoring node
return scoredNodes[0].node, nil
}
// calculateNodeScore calculates a score for a node based on multiple factors
func (a *AdaptiveStrategy) calculateNodeScore(node *Node, request *LoadRequest) float64 {
score := 100.0
if node.Metrics == nil {
return score
}
// Factor 1: Available capacity (higher is better)
availableCapacity := float64(node.Metrics.MaxJobs - node.Metrics.ActiveJobs)
if node.Metrics.MaxJobs > 0 {
capacityRatio := availableCapacity / float64(node.Metrics.MaxJobs)
score += capacityRatio * 50 // Up to 50 points for capacity
}
// Factor 2: System load (lower is better)
if node.Metrics.LoadAverage > 0 {
loadScore := 100.0 - (node.Metrics.LoadAverage * 10)
if loadScore < 0 {
loadScore = 0
}
score += loadScore * 0.3 // Up to 30 points for load
}
// Factor 3: Resource usage (lower is better)
cpuScore := 100.0 - node.Metrics.CPUUsage
memoryScore := 100.0 - node.Metrics.MemoryUsage
score += cpuScore * 0.1 // Up to 10 points for CPU
score += memoryScore * 0.1 // Up to 10 points for memory
// Factor 4: Priority-based adjustments
if node.Tags != nil {
if priority, ok := node.Tags["priority"]; ok {
switch priority {
case "high":
score += 20
case "low":
score -= 20
}
}
}
// Factor 5: Request-specific requirements
if request.Requirements != nil {
if arch, ok := request.Requirements["architecture"].(string); ok {
if nodeArch, ok := node.Capabilities["architecture"].(string); ok {
if arch == nodeArch {
score += 25 // Bonus for architecture match
}
}
}
}
return score
}
// GetName returns the strategy name
func (a *AdaptiveStrategy) GetName() string {
return "adaptive"
}
// StrategyFactory creates load balancing strategies
type StrategyFactory struct{}
// NewStrategyFactory creates a new strategy factory
func NewStrategyFactory() *StrategyFactory {
return &StrategyFactory{}
}
// CreateStrategy creates a strategy by name
func (sf *StrategyFactory) CreateStrategy(name string) (LoadBalancingStrategy, error) {
switch name {
case "round_robin":
return NewRoundRobinStrategy(), nil
case "least_connections":
return NewLeastConnectionsStrategy(), nil
case "weighted_round_robin":
return NewWeightedRoundRobinStrategy(), nil
case "random":
return NewRandomStrategy(), nil
case "least_response_time":
return NewLeastResponseTimeStrategy(), nil
case "ip_hash":
return NewIPHashStrategy(), nil
case "adaptive":
return NewAdaptiveStrategy(), nil
default:
return nil, fmt.Errorf("unknown strategy: %s", name)
}
}
// GetAvailableStrategies returns all available strategy names
func (sf *StrategyFactory) GetAvailableStrategies() []string {
return []string{
"round_robin",
"least_connections",
"weighted_round_robin",
"random",
"least_response_time",
"ip_hash",
"adaptive",
}
}