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
+32 -19
View File
@@ -80,24 +80,6 @@ func TestFailoverTimeBudgetExceeded(t *testing.T) {
}
}
func TestLoadAvoidanceFallbackContinuesToAvoidedCandidate(t *testing.T) {
candidates := []store.RuntimeModelCandidate{
{PlatformID: "available-candidate"},
{PlatformID: "avoided-full-candidate", LoadAvoided: true},
}
if !hasLoadAvoidanceFallback(candidates, 0, 99) {
t.Fatal("expected non-avoided candidate to fall back to later avoided candidate")
}
if hasLoadAvoidanceFallback(candidates, 1, 99) {
t.Fatal("avoided candidate should not force another load-avoidance fallback")
}
decision := loadAvoidanceFallbackDecision(&clients.ClientError{Code: "bad_request", StatusCode: 400, Retryable: false})
if !decision.Retry || decision.Reason != "load_avoidance_fallback" || decision.Action != "next" {
t.Fatalf("expected active load avoidance fallback to force next candidate, got %+v", decision)
}
}
func TestFailoverHardStopBeatsModelOverride(t *testing.T) {
runnerPolicy := store.RunnerPolicy{
Status: "active",
@@ -148,7 +130,7 @@ func TestFailoverPolicyAllowsModelOverride(t *testing.T) {
func TestLocalRateLimitWaitsInQueueWithoutRetryOrFailover(t *testing.T) {
err := &localRateLimitError{
clientErr: &clients.ClientError{Code: "rate_limit", Message: "local capacity exceeded", Retryable: true},
cause: &store.RateLimitExceededError{Metric: "concurrent", Retryable: true},
cause: &store.RateLimitExceededError{ScopeType: "user_group", Metric: "concurrent", Retryable: true},
}
retryDecision := retryDecisionForCandidate(store.RuntimeModelCandidate{}, err)
if retryDecision.Retry || retryDecision.Reason != "local_rate_limit_wait_queue" {
@@ -164,6 +146,37 @@ func TestLocalRateLimitWaitsInQueueWithoutRetryOrFailover(t *testing.T) {
}
}
func TestPlatformModelRateLimitRaceRotatesWithoutFailureEffect(t *testing.T) {
err := &localRateLimitError{
clientErr: &clients.ClientError{Code: "rate_limit", Message: "platform capacity raced", Retryable: true},
cause: &store.RateLimitExceededError{
ScopeType: "platform_model",
Metric: "concurrent",
Retryable: true,
RetryAfter: time.Second,
},
}
decision := resolveCandidateFailure(resolveCandidateFailureInput{
RunnerPolicy: store.RunnerPolicy{Status: "active"},
Err: err,
HasNextCandidate: true,
Async: true,
})
if decision.Route != "next" || decision.Effect != "none" || decision.Reason != "quota_race_rotated" {
t.Fatalf("platform quota race should rotate without mutation: %+v", decision)
}
last := resolveCandidateFailure(resolveCandidateFailureInput{
RunnerPolicy: store.RunnerPolicy{Status: "active"},
Err: err,
Async: true,
})
if last.Route != "requeue" || last.Effect != "none" {
t.Fatalf("last saturated platform should requeue: %+v", last)
}
}
func TestProviderAuthErrorsFailOverInsteadOfHardStop(t *testing.T) {
runnerPolicy := store.RunnerPolicy{
Status: "active",