feat(routing): 完善平台满载避让与故障轮转
为未配置并发上限的平台增加基于运行和等待任务数的软负载,并按非满载、有效优先级、缓存亲和与负载稳定排序。\n\n平台模型 RPM、TPM 和并发额度竞争失败时只轮转候选,不触发冷却、禁用或降级;用户组额度保持不可绕过。补齐异步冷却排队恢复、满载原因、选择原因与低基数指标。\n\n验证:go test ./...;go vet ./...;PostgreSQL 原子额度、准入队列、Worker 容量回收及故障策略 HTTP acceptance。
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user