实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
165 lines
5.9 KiB
Go
165 lines
5.9 KiB
Go
package capacitycontroller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCalculatePlanRespectsResourceDatabaseAndStepLimits(t *testing.T) {
|
|
input := PlanInput{
|
|
Queued: 1000, Running: 48, InstanceSlots: 24,
|
|
MemoryTargetPercent: 75, MemoryHardPercent: 85, CPUTargetPercent: 70,
|
|
DatabaseConnections: 80, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
|
|
SynchronousDatabasePeers: 1, ScaleUpEligible: true,
|
|
Sites: []SiteResources{
|
|
{
|
|
Site: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 6 << 30,
|
|
WorkerRequestMemoryBytes: 1 << 30,
|
|
AllocatableMilliCPU: 4000, UsedMilliCPU: 2000, WorkerRequestMilliCPU: 500,
|
|
},
|
|
{
|
|
Site: "hongkong", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 3 << 30,
|
|
WorkerRequestMemoryBytes: 1 << 30,
|
|
AllocatableMilliCPU: 4000, UsedMilliCPU: 500, WorkerRequestMilliCPU: 500,
|
|
},
|
|
},
|
|
}
|
|
plan := CalculatePlan(input)
|
|
if plan.RawDesired != 22 {
|
|
t.Fatalf("raw desired=%d, want 22", plan.RawDesired)
|
|
}
|
|
if plan.DesiredTotal != 4 {
|
|
t.Fatalf("desired total=%d, want step-limited 4: %+v", plan.DesiredTotal, plan)
|
|
}
|
|
if plan.Sites[0].Site != "hongkong" || plan.Sites[0].DesiredReplicas != 3 {
|
|
t.Fatalf("site allocation=%+v, want hongkong=3 ningbo=1", plan.Sites)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePlanFreezesScaleUpOnHardMemoryPressure(t *testing.T) {
|
|
plan := CalculatePlan(PlanInput{
|
|
Queued: 200, InstanceSlots: 24,
|
|
DatabaseConnections: 10, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
|
|
SynchronousDatabasePeers: 1, ScaleUpEligible: true,
|
|
Sites: []SiteResources{{
|
|
Site: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 7 << 30,
|
|
WorkerRequestMemoryBytes: 1 << 30,
|
|
}},
|
|
})
|
|
if plan.DesiredTotal != 1 || plan.FrozenReason != "node_memory_pressure" {
|
|
t.Fatalf("plan=%+v, want frozen at one replica", plan)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePlanReportsConfiguredMinimumOutsideResourceBudget(t *testing.T) {
|
|
plan := CalculatePlan(PlanInput{
|
|
Queued: 200, InstanceSlots: 24,
|
|
DatabaseConnections: 10, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
|
|
SynchronousDatabasePeers: 1, ScaleUpEligible: true,
|
|
Sites: []SiteResources{{
|
|
Site: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 13 << 29,
|
|
WorkerRequestMemoryBytes: 2 << 30,
|
|
AllocatableMilliCPU: 4000, UsedMilliCPU: 2800, WorkerRequestMilliCPU: 500,
|
|
}},
|
|
})
|
|
if plan.DesiredTotal != 1 || plan.FrozenReason != "site_resource_budget" {
|
|
t.Fatalf("plan=%+v, want the existing minimum preserved and expansion frozen", plan)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePlanWaitsForSafeScaleDownWindow(t *testing.T) {
|
|
input := PlanInput{
|
|
InstanceSlots: 24, DatabaseConnections: 20, DatabaseConnectionBudget: 150,
|
|
WorkerDatabasePoolMax: 24, SynchronousDatabasePeers: 1,
|
|
ScaleDownEligible: false,
|
|
Sites: []SiteResources{
|
|
{Site: "ningbo", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 4},
|
|
{Site: "hongkong", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 4},
|
|
},
|
|
}
|
|
plan := CalculatePlan(input)
|
|
if plan.DesiredTotal != 4 || plan.FrozenReason != "scale_down_stabilization" {
|
|
t.Fatalf("plan=%+v, want current replicas during stabilization", plan)
|
|
}
|
|
input.ScaleDownEligible = true
|
|
plan = CalculatePlan(input)
|
|
if plan.DesiredTotal != 2 {
|
|
t.Fatalf("desired total=%d, want min total 2", plan.DesiredTotal)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePlanAddsCapacityAcrossLabeledSiteNodes(t *testing.T) {
|
|
plan := CalculatePlan(PlanInput{
|
|
Queued: 500, InstanceSlots: 24,
|
|
MemoryTargetPercent: 75, MemoryHardPercent: 85, CPUTargetPercent: 70,
|
|
DatabaseConnections: 20, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 20,
|
|
SynchronousDatabasePeers: 1, ScaleUpEligible: true,
|
|
Sites: []SiteResources{{
|
|
Site: "hongkong", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 8,
|
|
WorkerRequestMemoryBytes: 1 << 30, WorkerRequestMilliCPU: 500,
|
|
Nodes: []NodeResources{
|
|
{
|
|
NodeName: "hk-a", CurrentReplicas: 1,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 3 << 30,
|
|
AllocatableMilliCPU: 4000, UsedMilliCPU: 1000,
|
|
},
|
|
{
|
|
NodeName: "hk-b", CurrentReplicas: 1,
|
|
AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 2 << 30,
|
|
AllocatableMilliCPU: 4000, UsedMilliCPU: 500,
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
if plan.Sites[0].ResourceMax != 7 {
|
|
t.Fatalf("resource max=%d, want database-budgeted multi-node maximum: %+v", plan.Sites[0].ResourceMax, plan)
|
|
}
|
|
if plan.DesiredTotal != 4 {
|
|
t.Fatalf("desired=%d, want two-wave step from 2 to 4", plan.DesiredTotal)
|
|
}
|
|
}
|
|
|
|
func TestCalculatePlanCapsReplicaMaximumByDeclaredDatabasePools(t *testing.T) {
|
|
plan := CalculatePlan(PlanInput{
|
|
Queued: 500, InstanceSlots: 24,
|
|
DatabaseConnections: 20, DatabaseConnectionBudget: 150,
|
|
NonWorkerConnectionBudget: 72, WorkerDatabasePoolMax: 32,
|
|
SynchronousDatabasePeers: 1, ScaleUpEligible: true,
|
|
Sites: []SiteResources{
|
|
{Site: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4},
|
|
{Site: "hongkong", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4},
|
|
},
|
|
})
|
|
if plan.DesiredTotal != 2 {
|
|
t.Fatalf("desired=%d, want the current 1+1 database-safe topology: %+v", plan.DesiredTotal, plan)
|
|
}
|
|
resourceMax := 0
|
|
for _, site := range plan.Sites {
|
|
resourceMax += site.ResourceMax
|
|
}
|
|
if resourceMax != 2 {
|
|
t.Fatalf("resource maximum=%d, want floor((150-72)/32)=2: %+v", resourceMax, plan)
|
|
}
|
|
}
|
|
|
|
func TestPlanJSONUsesAcceptanceContractFieldNames(t *testing.T) {
|
|
payload, err := json.Marshal(Plan{
|
|
RawDesired: 3,
|
|
Sites: []SitePlan{{Site: "hongkong", ResourceMax: 2}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(payload)
|
|
for _, field := range []string{`"rawDesired":3`, `"sites"`, `"site":"hongkong"`, `"resourceMax":2`} {
|
|
if !strings.Contains(text, field) {
|
|
t.Fatalf("plan JSON %s does not contain %s", text, field)
|
|
}
|
|
}
|
|
}
|