让 acceptance 候选负载读取同一 Run ID 下的并发、RPM 和 TPM 计数器,并在单个平台模型额度不足时继续尝试下一候选,生产与 canary 额度作用域保持不变。\n\n补齐本地容量控制器对宁波和香港测试 Worker 的最小 RBAC,避免初次应用生产反亲和配置时发生滚动死锁,并将突发负载对齐到完整限流窗口。\n\n验证:\n- go test ./internal/runner ./internal/store -count=1\n- go vet ./internal/runner ./internal/store\n- bash -n scripts/acceptance/local-cluster.sh scripts/acceptance/provider-burst.sh scripts/acceptance/run-local-acceptance.sh\n- shellcheck -x scripts/acceptance/local-cluster.sh scripts/acceptance/provider-burst.sh scripts/acceptance/run-local-acceptance.sh\n- kubectl apply --dry-run=client -f deploy/kubernetes/local-acceptance/capacity-controller-rbac.yaml
250 lines
8.7 KiB
Go
250 lines
8.7 KiB
Go
package runner
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func TestAdmissionInputFromSnapshotRefreshesWorkerCapacity(t *testing.T) {
|
|
admission := store.TaskAdmission{
|
|
TaskID: "task-1",
|
|
PlatformID: "platform-1",
|
|
PlatformModelID: "model-1",
|
|
UserGroupID: "group-1",
|
|
QueueKey: "queue-1",
|
|
Mode: "async",
|
|
Priority: 7,
|
|
Scopes: []store.AdmissionScope{
|
|
{ScopeType: "platform_model", ScopeKey: "model-1", ConcurrentLimit: 10},
|
|
{ScopeType: "worker_capacity", ScopeKey: "global", ConcurrentLimit: 48},
|
|
},
|
|
ReselectRequestedAt: time.Time{},
|
|
}
|
|
|
|
input := admissionInputFromSnapshot(admission, 24)
|
|
|
|
if input.TaskID != admission.TaskID || input.PlatformModelID != admission.PlatformModelID || len(input.Scopes) != 2 {
|
|
t.Fatalf("unexpected admission input: %+v", input)
|
|
}
|
|
if input.Scopes[1].ConcurrentLimit != 24 {
|
|
t.Fatalf("worker capacity=%v, want 24", input.Scopes[1].ConcurrentLimit)
|
|
}
|
|
if input.Scopes[0].ConcurrentLimit != 10 {
|
|
t.Fatalf("business scope changed: %+v", input.Scopes[0])
|
|
}
|
|
if admission.Scopes[1].ConcurrentLimit != 48 {
|
|
t.Fatalf("snapshot was mutated: %+v", admission.Scopes[1])
|
|
}
|
|
}
|
|
|
|
func TestDistributedAdmissionModelTypeBoundary(t *testing.T) {
|
|
for _, modelType := range []string{
|
|
"image_generate", "image_edit", "image_analysis", "image_vectorize",
|
|
"video_generate", "video_enhance", "image_to_video", "text_to_video",
|
|
"video_edit", "video_reference", "video_first_last_frame", "video_understanding", "omni_video", "omni",
|
|
"audio_generate", "audio_understanding", "text_to_speech", "voice_clone",
|
|
} {
|
|
if !distributedAdmissionModelType(modelType) {
|
|
t.Errorf("%s should use distributed admission", modelType)
|
|
}
|
|
}
|
|
for _, modelType := range []string{"text_generate", "embedding", "rerank", "", "unknown"} {
|
|
if distributedAdmissionModelType(modelType) {
|
|
t.Errorf("%s should preserve immediate execution/rate-limit behavior", modelType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAcceptanceAdmissionScopesIsolateRunWithoutChangingLimits(t *testing.T) {
|
|
input := []store.AdmissionScope{{
|
|
ScopeType: "platform_model",
|
|
ScopeKey: "model-1",
|
|
ConcurrentLimit: 10,
|
|
QueueLimit: 0,
|
|
MaxWaitSeconds: 0,
|
|
}, {
|
|
ScopeType: "worker_capacity",
|
|
ScopeKey: "global",
|
|
ConcurrentLimit: 48,
|
|
}}
|
|
|
|
got := acceptanceAdmissionScopes(store.GatewayTask{RunMode: "acceptance", AcceptanceRunID: "run-1"}, input)
|
|
|
|
if got[0].ConcurrentLimit != 10 || got[0].ScopeKey != "acceptance:run-1:model-1" {
|
|
t.Fatalf("protocol-emulated acceptance must preserve the limit in an isolated scope, got %+v", got[0])
|
|
}
|
|
if got[0].QueueLimit != acceptanceQueueLimit || got[0].MaxWaitSeconds != acceptanceQueueMaxWait {
|
|
t.Fatalf("acceptance must enable a bounded queue, got %+v", got[0])
|
|
}
|
|
if got[1].ConcurrentLimit != 48 || got[1].ScopeKey != "acceptance:run-1:global" {
|
|
t.Fatalf("acceptance worker capacity changed, got %+v", got[1])
|
|
}
|
|
if input[0].QueueLimit != 0 || input[0].MaxWaitSeconds != 0 {
|
|
t.Fatalf("acceptance queue overlay must not mutate the production scopes, got %+v", input[0])
|
|
}
|
|
}
|
|
|
|
func TestAcceptanceCanaryPreservesProductionConcurrency(t *testing.T) {
|
|
input := []store.AdmissionScope{{
|
|
ScopeType: "platform_model",
|
|
ScopeKey: "model-1",
|
|
ConcurrentLimit: 10,
|
|
}}
|
|
|
|
got := acceptanceAdmissionScopes(store.GatewayTask{RunMode: "acceptance_canary"}, input)
|
|
|
|
if got[0].ConcurrentLimit != 10 {
|
|
t.Fatalf("real acceptance canary must preserve production concurrency, got %+v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestAcceptanceInfrastructureReservationsIsolateEveryMetric(t *testing.T) {
|
|
input := []store.RateLimitReservation{
|
|
{ScopeType: "platform_model", ScopeKey: "model-1", Metric: "concurrent", Limit: 10},
|
|
{ScopeType: "platform_model", ScopeKey: "model-1", Metric: "rpm", Limit: 600},
|
|
{ScopeType: "user_group", ScopeKey: "group-1", Metric: "concurrent", Limit: 20},
|
|
}
|
|
|
|
got := acceptanceInfrastructureReservations(store.GatewayTask{RunMode: "acceptance", AcceptanceRunID: "run-1"}, input)
|
|
if len(got) != 3 || got[0].ScopeKey != "acceptance:run-1:model-1" || got[1].ScopeKey != "acceptance:run-1:model-1" || got[2].ScopeKey != "acceptance:run-1:group-1" {
|
|
t.Fatalf("unexpected acceptance reservations: %+v", got)
|
|
}
|
|
canary := acceptanceInfrastructureReservations(store.GatewayTask{RunMode: "acceptance_canary"}, input)
|
|
if len(canary) != len(input) {
|
|
t.Fatalf("real canary reservations changed: %+v", canary)
|
|
}
|
|
}
|
|
|
|
func TestAcceptanceQuotaScopePrefixMatchesIsolatedCounters(t *testing.T) {
|
|
acceptanceTask := store.GatewayTask{RunMode: "acceptance", AcceptanceRunID: "run-1"}
|
|
if got := acceptanceQuotaScopePrefix(acceptanceTask); got != "acceptance:run-1:" {
|
|
t.Fatalf("acceptance quota scope prefix=%q", got)
|
|
}
|
|
if got := acceptanceQuotaScopePrefix(store.GatewayTask{RunMode: "acceptance_canary", AcceptanceRunID: "run-1"}); got != "" {
|
|
t.Fatalf("canary must use production quota scope, got %q", got)
|
|
}
|
|
if got := acceptanceQuotaScopePrefix(store.GatewayTask{RunMode: "production"}); got != "" {
|
|
t.Fatalf("production quota scope changed, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAcceptanceAdmissionScopesLeaveProductionPolicyUnchanged(t *testing.T) {
|
|
input := []store.AdmissionScope{{
|
|
ScopeType: "platform_model",
|
|
ScopeKey: "model-1",
|
|
ConcurrentLimit: 10,
|
|
QueueLimit: 3,
|
|
MaxWaitSeconds: 7,
|
|
}}
|
|
|
|
got := acceptanceAdmissionScopes(store.GatewayTask{RunMode: "production"}, input)
|
|
|
|
if got[0].QueueLimit != 3 || got[0].MaxWaitSeconds != 7 || got[0].ConcurrentLimit != 10 {
|
|
t.Fatalf("production admission policy changed: %+v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestAsyncAdmissionBatchCoversCertifiedGlobalCapacity(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
globalHardLimit int
|
|
want int
|
|
}{
|
|
{globalHardLimit: 48, want: 48},
|
|
{globalHardLimit: 64, want: 64},
|
|
{globalHardLimit: 128, want: 64},
|
|
{globalHardLimit: 0, want: 64},
|
|
} {
|
|
if got := asyncAdmissionBatchLimit(testCase.globalHardLimit); got != testCase.want {
|
|
t.Fatalf("async admission batch limit for %d = %d, want %d", testCase.globalHardLimit, got, testCase.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPinCandidatesToTaskAdmissionPreservesAdmittedCandidate(t *testing.T) {
|
|
input := []store.RuntimeModelCandidate{
|
|
{PlatformID: "platform-a", PlatformModelID: "model-a"},
|
|
{PlatformID: "platform-b", PlatformModelID: "model-b"},
|
|
{PlatformID: "platform-c", PlatformModelID: "model-c"},
|
|
}
|
|
admission := &store.TaskAdmission{
|
|
Status: "admitted",
|
|
PlatformID: "platform-b",
|
|
PlatformModelID: "model-b",
|
|
}
|
|
|
|
got, pinned := pinCandidatesToTaskAdmission(input, admission)
|
|
|
|
if !pinned {
|
|
t.Fatal("expected admitted candidate to be pinned")
|
|
}
|
|
if got[0].PlatformModelID != "model-b" || got[1].PlatformModelID != "model-a" || got[2].PlatformModelID != "model-c" {
|
|
t.Fatalf("unexpected candidate order: %+v", got)
|
|
}
|
|
if input[0].PlatformModelID != "model-a" {
|
|
t.Fatalf("candidate pinning mutated the caller slice: %+v", input)
|
|
}
|
|
}
|
|
|
|
func TestPinCandidatesToTaskAdmissionPreservesWaitingCandidate(t *testing.T) {
|
|
input := []store.RuntimeModelCandidate{
|
|
{PlatformID: "platform-a", PlatformModelID: "model-a"},
|
|
{PlatformID: "platform-b", PlatformModelID: "model-b"},
|
|
}
|
|
admission := &store.TaskAdmission{
|
|
Status: "waiting",
|
|
PlatformID: "platform-b",
|
|
PlatformModelID: "model-b",
|
|
}
|
|
|
|
got, pinned := pinCandidatesToTaskAdmission(input, admission)
|
|
|
|
if !pinned || got[0].PlatformModelID != "model-b" {
|
|
t.Fatalf("waiting candidate was not pinned: pinned=%v candidates=%+v", pinned, got)
|
|
}
|
|
}
|
|
|
|
func TestPinCandidatesToTaskAdmissionAllowsRequestedReselection(t *testing.T) {
|
|
input := []store.RuntimeModelCandidate{
|
|
{PlatformID: "platform-a", PlatformModelID: "model-a"},
|
|
{PlatformID: "platform-b", PlatformModelID: "model-b"},
|
|
}
|
|
admission := &store.TaskAdmission{
|
|
Status: "waiting",
|
|
PlatformID: "platform-b",
|
|
PlatformModelID: "model-b",
|
|
ReselectRequestedAt: time.Now(),
|
|
}
|
|
|
|
got, pinned := pinCandidatesToTaskAdmission(input, admission)
|
|
|
|
if pinned {
|
|
t.Fatalf("reselection request unexpectedly pinned the old candidate: %+v", got)
|
|
}
|
|
if got[0].PlatformModelID != "model-a" {
|
|
t.Fatalf("reselection changed the sorted candidate order: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestPinCandidatesToTaskAdmissionIgnoresMissingCandidate(t *testing.T) {
|
|
input := []store.RuntimeModelCandidate{
|
|
{PlatformID: "platform-a", PlatformModelID: "model-a"},
|
|
{PlatformID: "platform-b", PlatformModelID: "model-b"},
|
|
}
|
|
admission := &store.TaskAdmission{
|
|
Status: "admitted",
|
|
PlatformID: "platform-c",
|
|
PlatformModelID: "model-c",
|
|
}
|
|
|
|
got, pinned := pinCandidatesToTaskAdmission(input, admission)
|
|
|
|
if pinned {
|
|
t.Fatalf("unexpected candidate pin for missing binding: %+v", got)
|
|
}
|
|
if got[0].PlatformModelID != "model-a" {
|
|
t.Fatalf("candidate order changed for missing binding: %+v", got)
|
|
}
|
|
}
|