2026-04-06 17:08:51 +08:00

178 lines
4.8 KiB
Go

package config
import (
"bytes"
"fmt"
"net"
"os"
"time"
"gopkg.in/yaml.v3"
)
const DefaultPath = "config/local.yaml"
// Config 汇总整个网关服务的运行参数。
type Config struct {
App AppConfig `yaml:"app"`
GRPC GRPCConfig `yaml:"grpc"`
}
// AppConfig 描述应用自身参数。
type AppConfig struct {
Name string `yaml:"name"`
Env string `yaml:"env"`
HTTPAddr string `yaml:"http_addr"`
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"`
}
// GRPCConfig 聚合所有下游 gRPC 服务配置。
type GRPCConfig struct {
User UpstreamConfig `yaml:"user"`
Pay UpstreamConfig `yaml:"pay"`
}
// UpstreamConfig 描述单个下游服务的节点和容错策略。
type UpstreamConfig struct {
// Target 保留兼容旧配置;内部会归一化到 Targets。
Target string `yaml:"target,omitempty"`
Targets []string `yaml:"targets"`
Timeout time.Duration `yaml:"timeout"`
Retry RetryConfig `yaml:"retry"`
CircuitBreaker CircuitBreakerConfig `yaml:"circuit_breaker"`
HealthCache HealthCacheConfig `yaml:"health_cache"`
}
// RetryConfig 控制单次请求在多个节点间的重试行为。
type RetryConfig struct {
MaxAttempts int `yaml:"max_attempts"`
Backoff time.Duration `yaml:"backoff"`
}
// CircuitBreakerConfig 控制连续失败后的断路时长。
type CircuitBreakerConfig struct {
FailureThreshold int `yaml:"failure_threshold"`
OpenTimeout time.Duration `yaml:"open_timeout"`
}
// HealthCacheConfig 控制 readiness 对下游健康状态缓存多久。
type HealthCacheConfig struct {
TTL time.Duration `yaml:"ttl"`
}
// Load 从 YAML 文件加载配置,并补齐默认值和校验。
func Load(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("read config file %s: %w", path, err)
}
cfg := defaultConfig()
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
if err := decoder.Decode(&cfg); err != nil {
return Config{}, fmt.Errorf("decode config file %s: %w", path, err)
}
cfg.normalize()
if err := validate(cfg); err != nil {
return Config{}, err
}
return cfg, nil
}
func defaultConfig() Config {
return Config{
App: AppConfig{
Name: "chatappgateway",
Env: "local",
HTTPAddr: ":8080",
ShutdownTimeout: 10 * time.Second,
},
GRPC: GRPCConfig{
User: defaultUpstreamConfig("127.0.0.1:9001"),
Pay: defaultUpstreamConfig("127.0.0.1:9002"),
},
}
}
func defaultUpstreamConfig(target string) UpstreamConfig {
return UpstreamConfig{
Targets: []string{target},
Timeout: 3 * time.Second,
Retry: RetryConfig{
MaxAttempts: 2,
Backoff: 200 * time.Millisecond,
},
CircuitBreaker: CircuitBreakerConfig{
FailureThreshold: 3,
OpenTimeout: 10 * time.Second,
},
HealthCache: HealthCacheConfig{
TTL: 2 * time.Second,
},
}
}
func (c *Config) normalize() {
c.GRPC.User = normalizeUpstreamConfig(c.GRPC.User)
c.GRPC.Pay = normalizeUpstreamConfig(c.GRPC.Pay)
}
func normalizeUpstreamConfig(cfg UpstreamConfig) UpstreamConfig {
if len(cfg.Targets) == 0 && cfg.Target != "" {
cfg.Targets = []string{cfg.Target}
}
cfg.Target = ""
return cfg
}
func validate(cfg Config) error {
if cfg.App.Name == "" {
return fmt.Errorf("app.name is required")
}
if _, err := net.ResolveTCPAddr("tcp", cfg.App.HTTPAddr); err != nil {
return fmt.Errorf("app.http_addr is invalid: %w", err)
}
if cfg.App.ShutdownTimeout <= 0 {
return fmt.Errorf("app.shutdown_timeout must be greater than 0")
}
if err := validateUpstream("grpc.user", cfg.GRPC.User); err != nil {
return err
}
if err := validateUpstream("grpc.pay", cfg.GRPC.Pay); err != nil {
return err
}
return nil
}
func validateUpstream(name string, cfg UpstreamConfig) error {
if len(cfg.Targets) == 0 {
return fmt.Errorf("%s.targets must contain at least one target", name)
}
for _, target := range cfg.Targets {
if _, err := net.ResolveTCPAddr("tcp", target); err != nil {
return fmt.Errorf("%s.targets contains invalid target %q: %w", name, target, err)
}
}
if cfg.Timeout <= 0 {
return fmt.Errorf("%s.timeout must be greater than 0", name)
}
if cfg.Retry.MaxAttempts <= 0 {
return fmt.Errorf("%s.retry.max_attempts must be greater than 0", name)
}
if cfg.Retry.Backoff < 0 {
return fmt.Errorf("%s.retry.backoff must be greater than or equal to 0", name)
}
if cfg.CircuitBreaker.FailureThreshold <= 0 {
return fmt.Errorf("%s.circuit_breaker.failure_threshold must be greater than 0", name)
}
if cfg.CircuitBreaker.OpenTimeout <= 0 {
return fmt.Errorf("%s.circuit_breaker.open_timeout must be greater than 0", name)
}
if cfg.HealthCache.TTL <= 0 {
return fmt.Errorf("%s.health_cache.ttl must be greater than 0", name)
}
return nil
}