All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 3m7s
Build and Release / Lint (push) Successful in 5m21s
Build and Release / Unit Tests (push) Successful in 5m46s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m44s
Build and Release / Build Binaries (amd64, darwin, linux-latest) (push) Successful in 4m4s
Build and Release / Build Binaries (arm64, darwin, linux-latest) (push) Successful in 3m23s
Build and Release / Build Binaries (arm64, linux, linux-latest) (push) Successful in 3m47s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h6m28s
169 lines
4.8 KiB
Go
169 lines
4.8 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sort"
|
|
|
|
"code.gitcaddy.com/server/models/db"
|
|
"code.gitcaddy.com/server/modules/log"
|
|
"code.gitcaddy.com/server/modules/optional"
|
|
"code.gitcaddy.com/server/modules/setting"
|
|
)
|
|
|
|
// RunnerBandwidthScore calculates a routing score for a runner
|
|
// Higher score = better for job assignment
|
|
// Score considers: bandwidth (primary), latency (secondary)
|
|
func RunnerBandwidthScore(runner *ActionRunner) float64 {
|
|
caps := runner.GetCapabilities()
|
|
if caps == nil {
|
|
return 50.0 // Default middling score if no capabilities
|
|
}
|
|
|
|
bw := caps.Bandwidth
|
|
if bw == nil || bw.DownloadMbps <= 0 {
|
|
// No bandwidth data - give a default middling score
|
|
return 50.0
|
|
}
|
|
|
|
// Base score from bandwidth (log scale to prevent huge gaps)
|
|
// 1 Mbps = 0, 10 Mbps = 33, 100 Mbps = 66, 1000 Mbps = 100
|
|
bandwidthScore := 0.0
|
|
if bw.DownloadMbps > 0 {
|
|
// Log10 scale: log10(1)=0, log10(10)=1, log10(100)=2, log10(1000)=3
|
|
logVal := math.Log10(bw.DownloadMbps)
|
|
if logVal < 0 {
|
|
logVal = 0
|
|
}
|
|
bandwidthScore = logVal * 33.3 // Scale to 0-100
|
|
}
|
|
|
|
// Latency penalty (subtract up to 20 points for high latency)
|
|
latencyPenalty := 0.0
|
|
if bw.LatencyMs > 10 {
|
|
// 0-10ms = no penalty, 10-50ms = small penalty, 50-200ms = bigger penalty
|
|
latencyPenalty = (bw.LatencyMs - 10) / 10.0
|
|
if latencyPenalty > 20 {
|
|
latencyPenalty = 20
|
|
}
|
|
}
|
|
|
|
return bandwidthScore - latencyPenalty
|
|
}
|
|
|
|
// ShouldAssignJobToRunner determines if a job should be assigned to this runner
|
|
// considering bandwidth-aware routing
|
|
// Returns: (shouldAssign bool, reason string)
|
|
func ShouldAssignJobToRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (bool, string) {
|
|
if !setting.Actions.BandwidthAwareRouting {
|
|
return true, "bandwidth routing disabled"
|
|
}
|
|
|
|
// Always assign if this runner is the only option
|
|
// (e.g., macos-only jobs when only mac runner available)
|
|
competingRunners := findCompetingRunners(ctx, runner, job)
|
|
if len(competingRunners) == 0 {
|
|
return true, "only matching runner"
|
|
}
|
|
|
|
// Calculate scores
|
|
myScore := RunnerBandwidthScore(runner)
|
|
|
|
// Find the best competing score
|
|
bestCompetingScore := 0.0
|
|
var bestCompetitor *ActionRunner
|
|
for _, r := range competingRunners {
|
|
score := RunnerBandwidthScore(r)
|
|
if score > bestCompetingScore {
|
|
bestCompetingScore = score
|
|
bestCompetitor = r
|
|
}
|
|
}
|
|
|
|
// If requesting runner is within threshold of best, allow assignment
|
|
// This prevents slow runners from being completely starved
|
|
threshold := setting.Actions.BandwidthScoreThreshold // default 20
|
|
if myScore >= bestCompetingScore-threshold {
|
|
return true, "within threshold of best runner"
|
|
}
|
|
|
|
// If the better runner is busy, allow this runner to take it
|
|
if bestCompetitor != nil && !isRunnerIdle(ctx, bestCompetitor) {
|
|
return true, "better runner is busy"
|
|
}
|
|
|
|
log.Debug("Runner %s (score: %.1f) deferred job to faster runner %s (score: %.1f)",
|
|
runner.Name, myScore, bestCompetitor.Name, bestCompetingScore)
|
|
|
|
return false, "faster runner available"
|
|
}
|
|
|
|
// findCompetingRunners finds other online runners that could handle this job
|
|
func findCompetingRunners(ctx context.Context, excludeRunner *ActionRunner, job *ActionRunJob) []*ActionRunner {
|
|
runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{
|
|
IsOnline: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to find competing runners: %v", err)
|
|
return nil
|
|
}
|
|
|
|
var competing []*ActionRunner
|
|
for _, r := range runners {
|
|
// Skip the requesting runner
|
|
if r.ID == excludeRunner.ID {
|
|
continue
|
|
}
|
|
// Skip offline runners
|
|
if !r.IsOnline() {
|
|
continue
|
|
}
|
|
// Check if this runner can handle the job
|
|
if r.CanMatchLabels(job.RunsOn) {
|
|
competing = append(competing, r)
|
|
}
|
|
}
|
|
|
|
return competing
|
|
}
|
|
|
|
// isRunnerIdle checks if a runner currently has no active tasks
|
|
func isRunnerIdle(ctx context.Context, runner *ActionRunner) bool {
|
|
count, err := db.GetEngine(ctx).
|
|
Where("runner_id = ? AND status = ?", runner.ID, StatusRunning).
|
|
Count(&ActionTask{})
|
|
if err != nil {
|
|
log.Error("Failed to check if runner %s is idle: %v", runner.Name, err)
|
|
return false
|
|
}
|
|
return count == 0
|
|
}
|
|
|
|
// GetRunnersForJobByBandwidth returns runners sorted by bandwidth score (best first)
|
|
func GetRunnersForJobByBandwidth(ctx context.Context, job *ActionRunJob) []*ActionRunner {
|
|
runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{
|
|
IsOnline: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to find runners for job: %v", err)
|
|
return nil
|
|
}
|
|
|
|
var matching []*ActionRunner
|
|
for _, r := range runners {
|
|
if r.CanMatchLabels(job.RunsOn) {
|
|
matching = append(matching, r)
|
|
}
|
|
}
|
|
|
|
// Sort by bandwidth score (highest first)
|
|
sort.Slice(matching, func(i, j int) bool {
|
|
return RunnerBandwidthScore(matching[i]) > RunnerBandwidthScore(matching[j])
|
|
})
|
|
|
|
return matching
|
|
}
|