feat(routing): 完善平台满载避让与故障轮转

为未配置并发上限的平台增加基于运行和等待任务数的软负载,并按非满载、有效优先级、缓存亲和与负载稳定排序。\n\n平台模型 RPM、TPM 和并发额度竞争失败时只轮转候选,不触发冷却、禁用或降级;用户组额度保持不可绕过。补齐异步冷却排队恢复、满载原因、选择原因与低基数指标。\n\n验证:go test ./...;go vet ./...;PostgreSQL 原子额度、准入队列、Worker 容量回收及故障策略 HTTP acceptance。
This commit is contained in:
2026-08-03 17:42:34 +08:00
parent b125599354
commit 8362d6d27a
15 changed files with 468 additions and 152 deletions
+138
View File
@@ -86,6 +86,144 @@ func TestRuntimeCandidateLoadUsesMaxLimitedMetric(t *testing.T) {
}
}
func TestRuntimeCandidateLoadUsesSoftConcurrencyWithoutLimit(t *testing.T) {
tests := []struct {
name string
active float64
waiting float64
stateRun float64
stateWait float64
wantCurrent float64
wantRatio float64
}{
{name: "idle", wantCurrent: 0, wantRatio: 0},
{name: "one active", active: 1, wantCurrent: 1, wantRatio: 0.5},
{name: "two from runtime state", stateRun: 2, wantCurrent: 2, wantRatio: 2.0 / 3.0},
{name: "uses maximum per phase without double counting", active: 1, waiting: 2, stateRun: 3, stateWait: 1, wantCurrent: 5, wantRatio: 5.0 / 6.0},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
candidate := RuntimeModelCandidate{}
applyRuntimeCandidateLoad(&candidate, runtimeCandidateLoadInput{
ConcurrentActive: test.active,
QueuedWaiting: test.waiting,
StateRunningCount: test.stateRun,
StateWaitingCount: test.stateWait,
})
if candidate.LoadLimited {
t.Fatal("soft concurrency must not become a hard limit")
}
if candidate.LoadMetrics.SoftCurrent != test.wantCurrent {
t.Fatalf("soft current=%v, want %v", candidate.LoadMetrics.SoftCurrent, test.wantCurrent)
}
if diff := candidate.LoadRatio - test.wantRatio; diff < -1e-9 || diff > 1e-9 {
t.Fatalf("load ratio=%v, want %v", candidate.LoadRatio, test.wantRatio)
}
})
}
}
func TestRuntimeCandidateLoadUsesSoftConcurrencyAlongsideRPM(t *testing.T) {
candidate := RuntimeModelCandidate{}
applyRuntimeCandidateLoad(&candidate, runtimeCandidateLoadInput{
Policy: map[string]any{"rules": []any{map[string]any{"metric": "rpm", "limit": 100}}},
RPMUsed: 20,
StateRunningCount: 4,
})
if !candidate.LoadLimited {
t.Fatal("rpm policy should remain a hard limit")
}
if candidate.LoadMetrics.ConcurrentLimit != 0 || candidate.LoadMetrics.ConcurrentRatio != 0.8 {
t.Fatalf("unexpected soft concurrent metric: %+v", candidate.LoadMetrics)
}
if candidate.LoadRatio != 0.8 {
t.Fatalf("load ratio=%v, want 0.8", candidate.LoadRatio)
}
}
func TestRuntimeCandidateWaitingMarksCandidateFull(t *testing.T) {
candidates := []RuntimeModelCandidate{
{
PlatformID: "higher-priority-waiting",
PlatformModelID: "model-a",
PlatformPriority: 10,
LoadRatio: 0.5,
LoadMetrics: RuntimeCandidateLoadMetrics{QueuedCount: 1},
},
{
PlatformID: "lower-priority-idle",
PlatformModelID: "model-b",
PlatformPriority: 20,
},
}
sortRuntimeModelCandidates(candidates)
if candidates[0].PlatformID != "lower-priority-idle" {
t.Fatalf("non-full lower-priority candidate should receive overflow: %+v", candidates)
}
if !candidates[1].LoadAvoided || !containsString(candidates[1].FullReasons, "waiting") {
t.Fatalf("waiting candidate should be marked full and avoided: %+v", candidates[1])
}
}
func TestRuntimeCandidateSortingUsesStableIDAfterLoadTies(t *testing.T) {
candidates := []RuntimeModelCandidate{
{PlatformID: "second", PlatformModelID: "model-b", PlatformPriority: 10},
{PlatformID: "first", PlatformModelID: "model-a", PlatformPriority: 10},
}
sortRuntimeModelCandidates(candidates)
if candidates[0].PlatformModelID != "model-a" {
t.Fatalf("stable candidate order=%+v", candidates)
}
}
func TestRuntimeCandidateSortingKeepsDeterministicFallbackWhenAllFull(t *testing.T) {
candidates := []RuntimeModelCandidate{
{
PlatformID: "second",
PlatformModelID: "model-b",
PlatformPriority: 10,
LoadMetrics: RuntimeCandidateLoadMetrics{
ConcurrentLimit: 1,
ConcurrentRatio: 1,
},
},
{
PlatformID: "first",
PlatformModelID: "model-a",
PlatformPriority: 10,
LoadMetrics: RuntimeCandidateLoadMetrics{
ConcurrentLimit: 1,
ConcurrentRatio: 1,
},
},
}
sortRuntimeModelCandidates(candidates)
if candidates[0].PlatformModelID != "model-a" || candidates[0].LoadAvoided || candidates[1].LoadAvoided {
t.Fatalf("all-full fallback should be stable without pretending a candidate was avoidable: %+v", candidates)
}
for _, candidate := range candidates {
if candidate.SelectionReason != "all_full_fallback" {
t.Fatalf("all-full candidate reason=%q, want all_full_fallback", candidate.SelectionReason)
}
}
}
func containsString(values []string, expected string) bool {
for _, value := range values {
if value == expected {
return true
}
}
return false
}
func TestRuntimeCandidateSortingAvoidsFullCandidatesButKeepsFallback(t *testing.T) {
candidates := []RuntimeModelCandidate{
{