feat(acceptance): 建立同构验收与弹性容量体系
实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
This commit is contained in:
@@ -59,6 +59,8 @@ type Config struct {
|
||||
BillingEngineMode string
|
||||
ProcessRole string
|
||||
DatabaseMaxConns int
|
||||
DatabaseCriticalMaxConns int
|
||||
DatabaseRiverMaxConns int
|
||||
DatabaseMinIdleConns int
|
||||
DatabaseMaxConnIdleSeconds int
|
||||
DatabaseIdleInTransactionTimeoutSeconds int
|
||||
@@ -76,6 +78,27 @@ type Config struct {
|
||||
AsyncWorkerHardLimit int
|
||||
AsyncWorkerInstanceHardLimit int
|
||||
AsyncWorkerRefreshIntervalSeconds int
|
||||
WorkerAutoscalingEnabled bool
|
||||
WorkerReplicasNingbo int
|
||||
WorkerReplicasHongkong int
|
||||
WorkerMinReplicasNingbo int
|
||||
WorkerMinReplicasHongkong int
|
||||
WorkerMaxReplicasNingbo int
|
||||
WorkerMaxReplicasHongkong int
|
||||
WorkerTargetOutstandingPerReplica int
|
||||
WorkerScaleUpWindowSeconds int
|
||||
WorkerScaleDownStabilizationSeconds int
|
||||
WorkerDrainTimeoutSeconds int
|
||||
NodeMemoryTargetPercent int
|
||||
NodeMemoryHardPercent int
|
||||
NodeCPUTargetPercent int
|
||||
PostgresConnectionBudget int
|
||||
PostgresNonWorkerConnectionBudget int
|
||||
WorkerDatabaseMaxConns int
|
||||
CapacityControllerNamespace string
|
||||
CapacityControllerAPIServer string
|
||||
CapacityControllerTokenFile string
|
||||
CapacityControllerCAFile string
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
@@ -131,6 +154,8 @@ func Load() Config {
|
||||
BillingEngineMode: strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
|
||||
ProcessRole: strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))),
|
||||
DatabaseMaxConns: envInt("AI_GATEWAY_DATABASE_MAX_CONNS", 0),
|
||||
DatabaseCriticalMaxConns: envOptionalIntValidated("AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS", 0),
|
||||
DatabaseRiverMaxConns: envOptionalIntValidated("AI_GATEWAY_DATABASE_RIVER_MAX_CONNS", 0),
|
||||
DatabaseMinIdleConns: envOptionalIntValidated("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", 0),
|
||||
DatabaseMaxConnIdleSeconds: envOptionalIntValidated("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS", 0),
|
||||
|
||||
@@ -140,27 +165,57 @@ func Load() Config {
|
||||
),
|
||||
DatabaseLockTimeoutSeconds: envOptionalIntValidated("AI_GATEWAY_DATABASE_LOCK_TIMEOUT_SECONDS", 30),
|
||||
|
||||
MediaRequestConcurrency: envIntValidated("AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY", 16),
|
||||
MediaMaterializationConcurrency: envIntValidated("AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY", 8),
|
||||
MediaOSSDirectEnabled: env("AI_GATEWAY_MEDIA_OSS_DIRECT_ENABLED", "false") == "true",
|
||||
MediaOSSEndpoint: strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_ENDPOINT", ""), "/"),
|
||||
MediaOSSBucket: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_BUCKET", "")),
|
||||
MediaOSSAccessKeyID: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_ID", "")),
|
||||
MediaOSSAccessKeySecret: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_SECRET", "")),
|
||||
MediaOSSPublicBaseURL: strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_PUBLIC_BASE_URL", ""), "/"),
|
||||
MediaOSSObjectPrefix: strings.Trim(env("AI_GATEWAY_MEDIA_OSS_OBJECT_PREFIX", "easyai-ai-gateway/media"), "/"),
|
||||
AsyncQueueWorkerEnabled: env("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "true") == "true",
|
||||
AsyncWorkerHardLimit: envIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
|
||||
MediaRequestConcurrency: envIntValidated("AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY", 16),
|
||||
MediaMaterializationConcurrency: envIntValidated("AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY", 8),
|
||||
MediaOSSDirectEnabled: env("AI_GATEWAY_MEDIA_OSS_DIRECT_ENABLED", "false") == "true",
|
||||
MediaOSSEndpoint: strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_ENDPOINT", ""), "/"),
|
||||
MediaOSSBucket: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_BUCKET", "")),
|
||||
MediaOSSAccessKeyID: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_ID", "")),
|
||||
MediaOSSAccessKeySecret: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_SECRET", "")),
|
||||
MediaOSSPublicBaseURL: strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_PUBLIC_BASE_URL", ""), "/"),
|
||||
MediaOSSObjectPrefix: strings.Trim(env("AI_GATEWAY_MEDIA_OSS_OBJECT_PREFIX", "easyai-ai-gateway/media"), "/"),
|
||||
AsyncQueueWorkerEnabled: env("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "true") == "true",
|
||||
AsyncWorkerHardLimit: envIntValidated(
|
||||
"AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT",
|
||||
envOptionalIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
|
||||
),
|
||||
AsyncWorkerInstanceHardLimit: envIntValidated("AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT", 32),
|
||||
AsyncWorkerRefreshIntervalSeconds: envIntValidated("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS", 5),
|
||||
WorkerAutoscalingEnabled: env("AI_GATEWAY_WORKER_AUTOSCALING_ENABLED", "false") == "true",
|
||||
WorkerReplicasNingbo: envOptionalIntValidated("AI_GATEWAY_WORKER_REPLICAS_NINGBO", 1),
|
||||
WorkerReplicasHongkong: envOptionalIntValidated("AI_GATEWAY_WORKER_REPLICAS_HONGKONG", 1),
|
||||
WorkerMinReplicasNingbo: envOptionalIntValidated("AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO", 1),
|
||||
WorkerMinReplicasHongkong: envOptionalIntValidated("AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG", 1),
|
||||
WorkerMaxReplicasNingbo: envOptionalIntValidated("AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO", 1),
|
||||
WorkerMaxReplicasHongkong: envOptionalIntValidated("AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG", 1),
|
||||
WorkerTargetOutstandingPerReplica: envOptionalIntValidated("AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA", 0),
|
||||
WorkerScaleUpWindowSeconds: envIntValidated("AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS", 20),
|
||||
WorkerScaleDownStabilizationSeconds: envIntValidated(
|
||||
"AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS",
|
||||
600,
|
||||
),
|
||||
WorkerDrainTimeoutSeconds: envIntValidated("AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS", 600),
|
||||
NodeMemoryTargetPercent: envIntValidated("AI_GATEWAY_NODE_MEMORY_TARGET_PERCENT", 75),
|
||||
NodeMemoryHardPercent: envIntValidated("AI_GATEWAY_NODE_MEMORY_HARD_PERCENT", 85),
|
||||
NodeCPUTargetPercent: envIntValidated("AI_GATEWAY_NODE_CPU_TARGET_PERCENT", 70),
|
||||
PostgresConnectionBudget: envIntValidated("AI_GATEWAY_POSTGRES_CONNECTION_BUDGET", 150),
|
||||
PostgresNonWorkerConnectionBudget: envIntValidated(
|
||||
"AI_GATEWAY_POSTGRES_NON_WORKER_CONNECTION_BUDGET",
|
||||
72,
|
||||
),
|
||||
WorkerDatabaseMaxConns: envIntValidated("AI_GATEWAY_WORKER_DATABASE_MAX_CONNS", 32),
|
||||
CapacityControllerNamespace: env("POD_NAMESPACE", env("AI_GATEWAY_CAPACITY_CONTROLLER_NAMESPACE", "easyai")),
|
||||
CapacityControllerAPIServer: env("AI_GATEWAY_CAPACITY_CONTROLLER_API_SERVER", "https://kubernetes.default.svc"),
|
||||
CapacityControllerTokenFile: env("AI_GATEWAY_CAPACITY_CONTROLLER_TOKEN_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
||||
CapacityControllerCAFile: env("AI_GATEWAY_CAPACITY_CONTROLLER_CA_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
switch strings.ToLower(strings.TrimSpace(c.ProcessRole)) {
|
||||
case "", "all", "api", "worker":
|
||||
case "", "all", "api", "worker", "capacity-controller":
|
||||
default:
|
||||
return errors.New("AI_GATEWAY_PROCESS_ROLE must be all, api, or worker")
|
||||
return errors.New("AI_GATEWAY_PROCESS_ROLE must be all, api, worker, or capacity-controller")
|
||||
}
|
||||
if c.DatabaseMaxConns < 0 || c.DatabaseMaxConns > 1000 {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MAX_CONNS must be between 1 and 1000 when configured")
|
||||
@@ -169,6 +224,19 @@ func (c Config) Validate() error {
|
||||
(c.DatabaseMaxConns > 0 && c.DatabaseMinIdleConns > c.DatabaseMaxConns) {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must be between 0 and AI_GATEWAY_DATABASE_MAX_CONNS")
|
||||
}
|
||||
if c.DatabaseCriticalMaxConns < 0 || c.DatabaseCriticalMaxConns > 64 ||
|
||||
(c.DatabaseMaxConns > 0 && c.DatabaseCriticalMaxConns >= c.DatabaseMaxConns) {
|
||||
return errors.New("AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS must be between 0 and less than AI_GATEWAY_DATABASE_MAX_CONNS")
|
||||
}
|
||||
if c.DatabaseRiverMaxConns < 0 || c.DatabaseRiverMaxConns > 256 ||
|
||||
(c.DatabaseMaxConns > 0 &&
|
||||
c.DatabaseCriticalMaxConns+c.DatabaseRiverMaxConns >= c.DatabaseMaxConns) {
|
||||
return errors.New("critical and River database pools must leave at least one execution connection")
|
||||
}
|
||||
if c.DatabaseMaxConns > 0 &&
|
||||
c.DatabaseMinIdleConns > c.DatabaseMaxConns-c.DatabaseCriticalMaxConns-c.DatabaseRiverMaxConns {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must not exceed the execution pool after reserving critical and River connections")
|
||||
}
|
||||
if c.DatabaseMaxConnIdleSeconds < 0 || c.DatabaseMaxConnIdleSeconds > 3600 {
|
||||
return errors.New("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS must be between 0 and 3600")
|
||||
}
|
||||
@@ -215,6 +283,50 @@ func (c Config) Validate() error {
|
||||
if c.AsyncWorkerRefreshIntervalSeconds < 1 {
|
||||
return errors.New("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS must be positive")
|
||||
}
|
||||
if c.WorkerReplicasNingbo < 0 || c.WorkerReplicasHongkong < 0 ||
|
||||
c.WorkerMinReplicasNingbo < 0 || c.WorkerMinReplicasHongkong < 0 ||
|
||||
c.WorkerMaxReplicasNingbo < c.WorkerMinReplicasNingbo ||
|
||||
c.WorkerMaxReplicasHongkong < c.WorkerMinReplicasHongkong ||
|
||||
c.WorkerMaxReplicasNingbo > 64 || c.WorkerMaxReplicasHongkong > 64 {
|
||||
return errors.New("Worker replica configuration must be between 0 and 64 with min not greater than max")
|
||||
}
|
||||
if c.WorkerAutoscalingEnabled &&
|
||||
(c.WorkerReplicasNingbo < c.WorkerMinReplicasNingbo || c.WorkerReplicasNingbo > c.WorkerMaxReplicasNingbo ||
|
||||
c.WorkerReplicasHongkong < c.WorkerMinReplicasHongkong || c.WorkerReplicasHongkong > c.WorkerMaxReplicasHongkong) {
|
||||
return errors.New("Worker bootstrap replicas must be within the autoscaling min/max range")
|
||||
}
|
||||
if c.WorkerTargetOutstandingPerReplica < 0 || c.WorkerTargetOutstandingPerReplica > 100000 {
|
||||
return errors.New("AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA must be between 0 and 100000")
|
||||
}
|
||||
if c.WorkerScaleUpWindowSeconds != 0 && (c.WorkerScaleUpWindowSeconds < 5 || c.WorkerScaleUpWindowSeconds > 600) {
|
||||
return errors.New("AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS must be between 5 and 600")
|
||||
}
|
||||
if c.WorkerScaleDownStabilizationSeconds != 0 &&
|
||||
(c.WorkerScaleDownStabilizationSeconds < 60 || c.WorkerScaleDownStabilizationSeconds > 86400) {
|
||||
return errors.New("AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS must be between 60 and 86400")
|
||||
}
|
||||
if c.WorkerDrainTimeoutSeconds != 0 && (c.WorkerDrainTimeoutSeconds < 60 || c.WorkerDrainTimeoutSeconds > 86400) {
|
||||
return errors.New("AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS must be between 60 and 86400")
|
||||
}
|
||||
if (c.NodeMemoryTargetPercent != 0 || c.NodeMemoryHardPercent != 0) &&
|
||||
(c.NodeMemoryTargetPercent < 50 || c.NodeMemoryTargetPercent > 90 ||
|
||||
c.NodeMemoryHardPercent <= c.NodeMemoryTargetPercent || c.NodeMemoryHardPercent > 95) {
|
||||
return errors.New("Worker node memory target/hard percentages must be ordered within 50..95")
|
||||
}
|
||||
if c.NodeCPUTargetPercent != 0 && (c.NodeCPUTargetPercent < 40 || c.NodeCPUTargetPercent > 90) {
|
||||
return errors.New("AI_GATEWAY_NODE_CPU_TARGET_PERCENT must be between 40 and 90")
|
||||
}
|
||||
if c.PostgresConnectionBudget < 0 || c.PostgresConnectionBudget > 1000 {
|
||||
return errors.New("AI_GATEWAY_POSTGRES_CONNECTION_BUDGET must be between 1 and 1000")
|
||||
}
|
||||
if c.PostgresNonWorkerConnectionBudget < 0 ||
|
||||
(c.PostgresConnectionBudget > 0 &&
|
||||
c.PostgresNonWorkerConnectionBudget >= c.PostgresConnectionBudget) {
|
||||
return errors.New("AI_GATEWAY_POSTGRES_NON_WORKER_CONNECTION_BUDGET must be below the PostgreSQL connection budget")
|
||||
}
|
||||
if c.WorkerDatabaseMaxConns < 0 || c.WorkerDatabaseMaxConns > 256 {
|
||||
return errors.New("AI_GATEWAY_WORKER_DATABASE_MAX_CONNS must be between 1 and 256")
|
||||
}
|
||||
if c.TaskProgressCallbackTimeoutMS != 0 && (c.TaskProgressCallbackTimeoutMS < 100 || c.TaskProgressCallbackTimeoutMS > 60000) {
|
||||
return errors.New("TASK_PROGRESS_CALLBACK_TIMEOUT_MS must be between 100 and 60000")
|
||||
}
|
||||
@@ -319,12 +431,17 @@ func (c Config) EffectiveProcessRole() string {
|
||||
}
|
||||
|
||||
func (c Config) RunsPublicHTTP() bool {
|
||||
return c.EffectiveProcessRole() != "worker"
|
||||
switch c.EffectiveProcessRole() {
|
||||
case "worker", "capacity-controller":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) RunsAsyncExecutionWorker() bool {
|
||||
switch c.EffectiveProcessRole() {
|
||||
case "api":
|
||||
case "api", "capacity-controller":
|
||||
return false
|
||||
case "worker":
|
||||
return true
|
||||
@@ -334,7 +451,16 @@ func (c Config) RunsAsyncExecutionWorker() bool {
|
||||
}
|
||||
|
||||
func (c Config) RunsBackgroundWorkers() bool {
|
||||
return c.EffectiveProcessRole() != "api"
|
||||
switch c.EffectiveProcessRole() {
|
||||
case "api", "capacity-controller":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) RunsCapacityController() bool {
|
||||
return c.EffectiveProcessRole() == "capacity-controller"
|
||||
}
|
||||
|
||||
type GlobalHTTPProxyStatus struct {
|
||||
|
||||
@@ -140,6 +140,57 @@ func TestProcessRolePrecedenceAndCompatibility(t *testing.T) {
|
||||
if !cfg.RunsPublicHTTP() || cfg.RunsAsyncExecutionWorker() || cfg.RunsBackgroundWorkers() {
|
||||
t.Fatalf("api role capabilities are inconsistent: %+v", cfg)
|
||||
}
|
||||
|
||||
t.Setenv("AI_GATEWAY_PROCESS_ROLE", "capacity-controller")
|
||||
cfg = Load()
|
||||
if cfg.RunsPublicHTTP() || cfg.RunsAsyncExecutionWorker() || cfg.RunsBackgroundWorkers() || !cfg.RunsCapacityController() {
|
||||
t.Fatalf("capacity controller role capabilities are inconsistent: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerAutoscalingConfiguration(t *testing.T) {
|
||||
t.Setenv("AI_GATEWAY_WORKER_AUTOSCALING_ENABLED", "true")
|
||||
t.Setenv("AI_GATEWAY_WORKER_REPLICAS_NINGBO", "1")
|
||||
t.Setenv("AI_GATEWAY_WORKER_REPLICAS_HONGKONG", "2")
|
||||
t.Setenv("AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO", "1")
|
||||
t.Setenv("AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG", "1")
|
||||
t.Setenv("AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO", "2")
|
||||
t.Setenv("AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG", "3")
|
||||
cfg := Load()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("valid autoscaling configuration was rejected: %v", err)
|
||||
}
|
||||
if !cfg.WorkerAutoscalingEnabled || cfg.WorkerReplicasHongkong != 2 || cfg.WorkerMaxReplicasHongkong != 3 {
|
||||
t.Fatalf("autoscaling configuration=%+v", cfg)
|
||||
}
|
||||
cfg.WorkerReplicasHongkong = 4
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "bootstrap") {
|
||||
t.Fatalf("Validate() error=%v, want bootstrap bound failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrefersWorkerGlobalHardLimitAlias(t *testing.T) {
|
||||
t.Setenv("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", "48")
|
||||
t.Setenv("AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT", "96")
|
||||
cfg := Load()
|
||||
if cfg.AsyncWorkerHardLimit != 96 {
|
||||
t.Fatalf("global worker hard limit=%d, want 96", cfg.AsyncWorkerHardLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePostgresReplicaBudgets(t *testing.T) {
|
||||
cfg := Load()
|
||||
if cfg.PostgresConnectionBudget != 150 || cfg.PostgresNonWorkerConnectionBudget != 72 {
|
||||
t.Fatalf(
|
||||
"PostgreSQL budgets=%d/%d, want 150/72",
|
||||
cfg.PostgresConnectionBudget,
|
||||
cfg.PostgresNonWorkerConnectionBudget,
|
||||
)
|
||||
}
|
||||
cfg.PostgresNonWorkerConnectionBudget = cfg.PostgresConnectionBudget
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "NON_WORKER") {
|
||||
t.Fatalf("Validate() error=%v, want non-Worker budget failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
|
||||
@@ -171,6 +222,12 @@ func TestValidateProcessRoleAndDatabasePool(t *testing.T) {
|
||||
t.Fatalf("Validate() error = %v, want invalid database min idle conns", err)
|
||||
}
|
||||
cfg.DatabaseMinIdleConns = 4
|
||||
cfg.DatabaseCriticalMaxConns = 4
|
||||
cfg.DatabaseRiverMaxConns = 12
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "River") {
|
||||
t.Fatalf("Validate() error = %v, want database pool budget failure", err)
|
||||
}
|
||||
cfg.DatabaseRiverMaxConns = 4
|
||||
t.Setenv("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", "not-an-integer")
|
||||
if err := Load().Validate(); err == nil || !strings.Contains(err.Error(), "DATABASE_MIN_IDLE_CONNS") {
|
||||
t.Fatalf("Validate() error = %v, want invalid non-integer database min idle conns", err)
|
||||
|
||||
Reference in New Issue
Block a user