Files
tokens-reef/backend/internal/config/config_validate_gateway.go
User a4eb4d4c3a refactor(config): split config.go into modular files
Split the monolithic config.go (~120KB) into focused modules:
- auth.go: JWT, TOTP, Turnstile, RateLimit configs
- billing.go: Billing and Pricing configs
- database.go: Database and Redis configs
- gateway.go: Gateway and Upstream configs
- gateway_sub.go: Gateway sub-configurations
- ops_and_cache.go: Ops and Cache configs
- platforms.go: Platform-specific configs
- security.go: Security-related configs
- server.go: Server configuration
- config_defaults.go: Default values
- config_defaults_detail.go: Detailed defaults
- config_helpers.go: Helper functions
- config_validate.go: Validation logic
- config_validate_gateway.go: Gateway validation

This improves:
- Code maintainability and readability
- Faster compilation (smaller files)
- Easier navigation and debugging
- Better separation of concerns
2026-04-17 07:22:55 +08:00

160 lines
9.0 KiB
Go

package config
import (
"fmt"
"log/slog"
"strings"
)
func validateGateway(g *GatewayConfig) error {
if g.MaxBodySize <= 0 { return fmt.Errorf("gateway.max_body_size must be positive") }
if g.UpstreamResponseReadMaxBytes <= 0 { return fmt.Errorf("upstream_response_read_max_bytes must be positive") }
if g.ProxyProbeResponseReadMaxBytes <= 0 { return fmt.Errorf("proxy_probe_response_read_max_bytes must be positive") }
if strings.TrimSpace(g.ConnectionPoolIsolation) != "" {
switch g.ConnectionPoolIsolation {
case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
default: return fmt.Errorf("invalid connection_pool_isolation")
}
}
if g.MaxIdleConns <= 0 || g.MaxIdleConnsPerHost <= 0 || g.MaxConnsPerHost < 0 {
return fmt.Errorf("gateway connection pool fields invalid")
}
if g.IdleConnTimeoutSeconds <= 0 { return fmt.Errorf("idle_conn_timeout_seconds must be positive") }
if g.IdleConnTimeoutSeconds > 180 { slog.Warn("idle_conn_timeout_seconds is high; consider 60-120") }
if g.MaxUpstreamClients <= 0 { return fmt.Errorf("max_upstream_clients must be positive") }
if g.ClientIdleTTLSeconds <= 0 { return fmt.Errorf("client_idle_ttl_seconds must be positive") }
if g.ConcurrencySlotTTLMinutes <= 0 { return fmt.Errorf("concurrency_slot_ttl_minutes must be positive") }
if err := validateGatewayStream(g); err != nil { return err }
if err := validateGatewayOpenAIWS(&g.OpenAIWS); err != nil { return err }
if err := validateGatewayUsageRecord(&g.UsageRecord); err != nil { return err }
if err := validateGatewayScheduling(&g.Scheduling); err != nil { return err }
if g.UserGroupRateCacheTTLSeconds <= 0 { return fmt.Errorf("user_group_rate_cache_ttl_seconds must be positive") }
if g.ModelsListCacheTTLSeconds < 10 || g.ModelsListCacheTTLSeconds > 30 {
return fmt.Errorf("models_list_cache_ttl_seconds must be between 10-30")
}
return nil
}
func validateGatewayStream(g *GatewayConfig) error {
if g.StreamDataIntervalTimeout < 0 { return fmt.Errorf("stream_data_interval_timeout must be non-negative") }
if g.StreamDataIntervalTimeout != 0 && (g.StreamDataIntervalTimeout < 30 || g.StreamDataIntervalTimeout > 300) {
return fmt.Errorf("stream_data_interval_timeout must be 0 or between 30-300 seconds")
}
if g.StreamKeepaliveInterval < 0 { return fmt.Errorf("stream_keepalive_interval must be non-negative") }
if g.StreamKeepaliveInterval != 0 && (g.StreamKeepaliveInterval < 5 || g.StreamKeepaliveInterval > 30) {
return fmt.Errorf("stream_keepalive_interval must be 0 or between 5-30 seconds")
}
if g.MaxLineSize < 0 { return fmt.Errorf("max_line_size must be non-negative") }
if g.MaxLineSize != 0 && g.MaxLineSize < 1024*1024 { return fmt.Errorf("max_line_size must be at least 1MB") }
return nil
}
func validateGatewayOpenAIWS(ws *GatewayOpenAIWSConfig) error {
// Basic numeric checks
checks := []struct{ name string; val int64 }{
{"max_conns_per_account", int64(ws.MaxConnsPerAccount)},
{"dial_timeout_seconds", int64(ws.DialTimeoutSeconds)},
{"read_timeout_seconds", int64(ws.ReadTimeoutSeconds)},
{"write_timeout_seconds", int64(ws.WriteTimeoutSeconds)},
{"pool_target_utilization", int64(ws.PoolTargetUtilization * 100)}, // scale for comparison
{"queue_limit_per_conn", int64(ws.QueueLimitPerConn)},
{"event_flush_batch_size", int64(ws.EventFlushBatchSize)},
{"lb_top_k", int64(ws.LBTopK)},
{"sticky_session_ttl_seconds", int64(ws.StickySessionTTLSeconds)},
{"sticky_response_id_ttl_seconds", int64(ws.StickyResponseIDTTLSeconds)},
{"max_conns_per_account", int64(ws.MaxConnsPerAccount)},
}
for _, c := range checks {
if c.val <= 0 { return fmt.Errorf("openai_ws.%s must be positive", c.name) }
}
if ws.MinIdlePerAccount < 0 || ws.MaxIdlePerAccount < 0 {
return fmt.Errorf("openai_ws idle per-account fields must be non-negative")
}
if ws.MinIdlePerAccount > ws.MaxIdlePerAccount { return fmt.Errorf("min_idle_per_account must be <= max_idle_per_account") }
if ws.MaxIdlePerAccount > ws.MaxConnsPerAccount { return fmt.Errorf("max_idle_per_account must be <= max_conns_per_account") }
if ws.OAuthMaxConnsFactor <= 0 || ws.APIKeyMaxConnsFactor <= 0 {
return fmt.Errorf("openai_ws conns factor must be positive")
}
if ws.PoolTargetUtilization <= 0 || ws.PoolTargetUtilization > 1 {
return fmt.Errorf("pool_target_utilization must be within (0,1]")
}
if ws.EventFlushIntervalMS < 0 || ws.PrewarmCooldownMS < 0 ||
ws.FallbackCooldownSeconds < 0 || ws.RetryBackoffInitialMS < 0 ||
ws.RetryBackoffMaxMS < 0 || ws.RetryTotalBudgetMS < 0 {
return fmt.Errorf("openai_ws timeout/retry fields must be non-negative")
}
if ws.RetryBackoffInitialMS > 0 && ws.RetryBackoffMaxMS > 0 && ws.RetryBackoffMaxMS < ws.RetryBackoffInitialMS {
return fmt.Errorf("retry_backoff_max_ms >= retry_backoff_initial_ms")
}
if ws.RetryJitterRatio < 0 || ws.RetryJitterRatio > 1 { return fmt.Errorf("retry_jitter_ratio within [0,1]") }
if ws.PayloadLogSampleRate < 0 || ws.PayloadLogSampleRate > 1 { return fmt.Errorf("payload_log_sample_rate within [0,1]") }
if ws.StickyPreviousResponseTTLSeconds < 0 { return fmt.Errorf("sticky_previous_response_ttl_seconds must be non-negative") }
if ws.SchedulerScoreWeights.Priority < 0 || ws.SchedulerScoreWeights.Load < 0 ||
ws.SchedulerScoreWeights.Queue < 0 || ws.SchedulerScoreWeights.ErrorRate < 0 || ws.SchedulerScoreWeights.TTFT < 0 {
return fmt.Errorf("scheduler_score_weights must be non-negative")
}
weightSum := ws.SchedulerScoreWeights.Priority + ws.SchedulerScoreWeights.Load + ws.SchedulerScoreWeights.Queue +
ws.SchedulerScoreWeights.ErrorRate + ws.SchedulerScoreWeights.TTFT
if weightSum <= 0 { return fmt.Errorf("scheduler_score_weights must not all be zero") }
// Ingress mode
if mode := strings.ToLower(strings.TrimSpace(ws.IngressModeDefault)); mode != "" {
switch mode { case "off", "ctx_pool", "passthrough": default: return fmt.Errorf("ingress_mode_default must be off|ctx_pool|passthrough") }
}
if mode := strings.ToLower(strings.TrimSpace(ws.StoreDisabledConnMode)); mode != "" {
switch mode { case "strict", "adaptive", "off": default: return fmt.Errorf("store_disabled_conn_mode must be strict|adaptive|off") }
}
return nil
}
func validateGatewayUsageRecord(ur *GatewayUsageRecordConfig) error {
if ur.WorkerCount <= 0 || ur.QueueSize <= 0 || ur.TaskTimeoutSeconds <= 0 {
return fmt.Errorf("usage_record worker/queue/timeout must be positive")
}
switch ur.OverflowPolicy {
case UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync:
default: return fmt.Errorf("invalid overflow_policy")
}
if ur.OverflowSamplePercent < 0 || ur.OverflowSamplePercent > 100 {
return fmt.Errorf("overflow_sample_percent must be 0-100")
}
if strings.EqualFold(ur.OverflowPolicy, UsageRecordOverflowPolicySample) && ur.OverflowSamplePercent <= 0 {
return fmt.Errorf("overflow_sample_percent must be positive when policy=sample")
}
if !ur.AutoScaleEnabled { return nil }
if ur.AutoScaleMinWorkers <= 0 || ur.AutoScaleMaxWorkers <= 0 { return fmt.Errorf("auto_scale workers must be positive") }
if ur.AutoScaleMaxWorkers < ur.AutoScaleMinWorkers { return fmt.Errorf("auto_scale_max >= auto_scale_min") }
if ur.WorkerCount < ur.AutoScaleMinWorkers || ur.WorkerCount > ur.AutoScaleMaxWorkers {
return fmt.Errorf("worker_count between auto_scale_min and max")
}
if ur.AutoScaleUpQueuePercent <= 0 || ur.AutoScaleUpQueuePercent > 100 { return fmt.Errorf("auto_scale_up_queue_percent 1-100") }
if ur.AutoScaleDownQueuePercent < 0 || ur.AutoScaleDownQueuePercent >= 100 { return fmt.Errorf("auto_scale_down_queue_percent 0-99") }
if ur.AutoScaleDownQueuePercent >= ur.AutoScaleUpQueuePercent { return fmt.Errorf("down_queue_percent < up_queue_percent") }
if ur.AutoScaleUpStep <= 0 || ur.AutoScaleDownStep <= 0 { return fmt.Errorf("auto_scale steps must be positive") }
if ur.AutoScaleCheckIntervalSeconds <= 0 { return fmt.Errorf("auto_scale_check_interval_seconds must be positive") }
if ur.AutoScaleCooldownSeconds < 0 { return fmt.Errorf("auto_scale_cooldown_seconds must be non-negative") }
return nil
}
func validateGatewayScheduling(s *GatewaySchedulingConfig) error {
if s.StickySessionMaxWaiting <= 0 || s.StickySessionWaitTimeout <= 0 ||
s.FallbackWaitTimeout <= 0 || s.FallbackMaxWaiting <= 0 ||
s.SnapshotMGetChunkSize <= 0 || s.SnapshotWriteChunkSize <= 0 {
return fmt.Errorf("scheduling core fields must be positive")
}
if s.SlotCleanupInterval < 0 || s.DbFallbackTimeoutSeconds < 0 || s.DbFallbackMaxQPS < 0 {
return fmt.Errorf("scheduling optional fields must be non-negative")
}
if s.OutboxPollIntervalSeconds <= 0 || s.OutboxLagRebuildFailures <= 0 || s.OutboxBacklogRebuildRows < 0 {
return fmt.Errorf("outbox fields must be non-negative or positive as documented")
}
if s.OutboxLagWarnSeconds < 0 || s.OutboxLagRebuildSeconds < 0 || s.FullRebuildIntervalSeconds < 0 {
return fmt.Errorf("outbox timing fields must be non-negative")
}
if s.OutboxLagWarnSeconds > 0 && s.OutboxLagRebuildSeconds > 0 && s.OutboxLagRebuildSeconds < s.OutboxLagWarnSeconds {
return fmt.Errorf("outbox_lag_rebuild_seconds >= outbox_lag_warn_seconds")
}
return nil
}