178 lines
4.8 KiB
Go
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
|
|
}
|