Files
easyai-ai-gateway/apps/api/internal/executionpool/selector_test.go
T
wangbo 7786692d32 feat(routing): 引入多执行池智能调度
将 Worker 发现、路由画像、容量与执行传输抽象为平台无关接口,新增 Kubernetes 和静态容量适配器,并以 shadow 模式接入生产配置。

实现网络与容量评分、路由防抖、池队列、同步 Worker 租约、一次性执行令牌,以及提交状态不明时禁止重复分配的安全语义。

新增 0105 兼容迁移、管理接口、指标、OpenAPI 和回归测试。已执行全量 Go 测试、go vet、OpenAPI、迁移安全、Compose 与 Kustomize 验证。
2026-08-05 22:25:37 +08:00

87 lines
2.8 KiB
Go

package executionpool
import (
"errors"
"testing"
"time"
)
func TestSelectorRejectsUnreachableAndPrefersNetwork(t *testing.T) {
now := time.Now()
selector := NewSelector()
decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
candidate("slow", RouteHealthy, now, 120*time.Millisecond, 8, 1),
candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1),
candidate("down", RouteUnreachable, now, time.Millisecond, 32, 0),
}})
if err != nil {
t.Fatal(err)
}
if decision.PoolID != "fast" {
t.Fatalf("pool=%s, want fast", decision.PoolID)
}
if decision.Rejected["down"] != "route_unreachable" {
t.Fatalf("down rejection=%q", decision.Rejected["down"])
}
}
func TestSelectorUsesCapacityAsHardGate(t *testing.T) {
now := time.Now()
selector := NewSelector()
decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
candidate("fast-full", RouteHealthy, now, 10*time.Millisecond, 2, 2),
candidate("slower-free", RouteHealthy, now, 80*time.Millisecond, 8, 1),
}})
if err != nil {
t.Fatal(err)
}
if decision.PoolID != "slower-free" {
t.Fatalf("pool=%s, want slower-free", decision.PoolID)
}
}
func TestSelectorRejectsUnknownAndStale(t *testing.T) {
now := time.Now()
selector := NewSelector()
_, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
candidate("unknown", RouteUnknown, now, time.Millisecond, 8, 0),
candidate("stale", RouteHealthy, now.Add(-time.Minute), time.Millisecond, 8, 0),
}})
if !errors.Is(err, ErrNoEligiblePool) {
t.Fatalf("err=%v, want ErrNoEligiblePool", err)
}
}
func TestSelectorHonorsHysteresis(t *testing.T) {
now := time.Now()
selector := NewSelector()
fast := candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1)
fast.BetterWindows = 3
current := candidate("current", RouteHealthy, now, 80*time.Millisecond, 8, 1)
decision, err := selector.Select(SelectionRequest{
Now: now, Candidates: []SelectionCandidate{fast, current},
CurrentPoolID: "current", CurrentPoolSince: now.Add(-time.Minute),
})
if err != nil {
t.Fatal(err)
}
if decision.PoolID != "current" {
t.Fatalf("pool=%s, want current during dwell", decision.PoolID)
}
}
func candidate(id string, state RouteState, sampled time.Time, latency time.Duration, capacity, active int) SelectionCandidate {
return SelectionCandidate{
Pool: ExecutionPool{ID: id, State: PoolActive}, CapabilityMatched: true,
Health: RouteHealth{
PoolID: id, State: state, SuccessRate: 1, ConnectTLSP95: latency,
FirstByteP95: latency, UploadBytesPerSecond: 10 * 1024 * 1024,
JitterP95: latency / 10, SampledAt: sampled, ExpiresAt: sampled.Add(45 * time.Second),
},
Capacity: CapacitySnapshot{
PoolID: id, WorkerCount: 1, SafeCapacity: capacity, ActiveTasks: active,
ResourceHeadroom: 0.8, Stability: 1,
},
}
}