package config import ( "errors" "fmt" "gopkg.in/yaml.v3" "os" "runtime" "strconv" "strings" "time" ) const ( ModeResolver = "resolver" ModeMirror = "mirror" ModeThin = "thin" VerifyFull = "full" VerifyLazy = "lazy" VerifyStateOnly = "state-only" ) type Config struct { Mode string `yaml:"mode"` DataDir string `yaml:"data_dir"` PLCSource string `yaml:"plc_source"` VerifyPolicy string `yaml:"verify"` ZstdLevel int `yaml:"zstd_level"` BlockSizeMB int `yaml:"block_size_mb"` CheckpointInterval uint64 `yaml:"checkpoint_interval"` CommitBatchSize int `yaml:"commit_batch_size"` VerifyWorkers int `yaml:"verify_workers"` ExportPageSize int `yaml:"export_page_size"` ReplayTrace bool `yaml:"replay_trace"` ThinCacheTTL time.Duration `yaml:"thin_cache_ttl"` ThinCacheMaxEntries int `yaml:"thin_cache_max_entries"` ListenAddr string `yaml:"listen_addr"` MirrorPrivateKeyPath string `yaml:"mirror_private_key_path"` PollInterval time.Duration `yaml:"poll_interval"` RequestTimeout time.Duration `yaml:"request_timeout"` RateLimit RateLimit `yaml:"rate_limit"` HTTPRetryMaxAttempts int `yaml:"http_retry_max_attempts"` HTTPRetryBaseDelay time.Duration `yaml:"http_retry_base_delay"` HTTPRetryMaxDelay time.Duration `yaml:"http_retry_max_delay"` } type RateLimit struct { ResolveRPS float64 `yaml:"resolve_rps"` ResolveBurst int `yaml:"resolve_burst"` ProofRPS float64 `yaml:"proof_rps"` ProofBurst int `yaml:"proof_burst"` } func Default() Config { return Config{ Mode: ModeMirror, DataDir: "./data", PLCSource: "https://plc.directory", VerifyPolicy: VerifyFull, ZstdLevel: 9, BlockSizeMB: 8, CheckpointInterval: 100000, CommitBatchSize: 128, VerifyWorkers: runtime.NumCPU(), ExportPageSize: 1000, ReplayTrace: false, ThinCacheTTL: 24 * time.Hour, ThinCacheMaxEntries: 100000, ListenAddr: ":8080", MirrorPrivateKeyPath: "./mirror.key", PollInterval: 5 * time.Second, RequestTimeout: 10 * time.Second, RateLimit: RateLimit{ ResolveRPS: 30, ResolveBurst: 60, ProofRPS: 10, ProofBurst: 20, }, HTTPRetryMaxAttempts: 8, HTTPRetryBaseDelay: 250 * time.Millisecond, HTTPRetryMaxDelay: 10 * time.Second, } } func Load(path string) (Config, error) { cfg := Default() if path != "" { b, err := os.ReadFile(path) if err != nil { if !errors.Is(err, os.ErrNotExist) { return Config{}, fmt.Errorf("read config: %w", err) } } else { if err := yaml.Unmarshal(b, &cfg); err != nil { return Config{}, fmt.Errorf("parse config: %w", err) } } } applyEnv(&cfg) if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func applyEnv(cfg *Config) { setString := func(key string, dst *string) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { *dst = v } } setFloat64 := func(key string, dst *float64) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if n, err := strconv.ParseFloat(v, 64); err == nil { *dst = n } } } setInt := func(key string, dst *int) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if n, err := strconv.Atoi(v); err == nil { *dst = n } } } setUint64 := func(key string, dst *uint64) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if n, err := strconv.ParseUint(v, 10, 64); err == nil { *dst = n } } } setDuration := func(key string, dst *time.Duration) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if d, err := time.ParseDuration(v); err == nil { *dst = d } } } setBool := func(key string, dst *bool) { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if b, err := strconv.ParseBool(v); err == nil { *dst = b } } } setString("PLUTIA_MODE", &cfg.Mode) setString("PLUTIA_DATA_DIR", &cfg.DataDir) setString("PLUTIA_PLC_SOURCE", &cfg.PLCSource) setString("PLUTIA_VERIFY", &cfg.VerifyPolicy) setInt("PLUTIA_ZSTD_LEVEL", &cfg.ZstdLevel) setInt("PLUTIA_BLOCK_SIZE_MB", &cfg.BlockSizeMB) setInt("PLUTIA_COMMIT_BATCH_SIZE", &cfg.CommitBatchSize) setInt("PLUTIA_VERIFY_WORKERS", &cfg.VerifyWorkers) setInt("PLUTIA_EXPORT_PAGE_SIZE", &cfg.ExportPageSize) setBool("PLUTIA_REPLAY_TRACE", &cfg.ReplayTrace) setDuration("PLUTIA_THIN_CACHE_TTL", &cfg.ThinCacheTTL) setInt("PLUTIA_THIN_CACHE_MAX_ENTRIES", &cfg.ThinCacheMaxEntries) setUint64("PLUTIA_CHECKPOINT_INTERVAL", &cfg.CheckpointInterval) setString("PLUTIA_LISTEN_ADDR", &cfg.ListenAddr) setString("PLUTIA_MIRROR_PRIVATE_KEY_PATH", &cfg.MirrorPrivateKeyPath) setDuration("PLUTIA_POLL_INTERVAL", &cfg.PollInterval) setDuration("PLUTIA_REQUEST_TIMEOUT", &cfg.RequestTimeout) setFloat64("PLUTIA_RATE_LIMIT_RESOLVE_RPS", &cfg.RateLimit.ResolveRPS) setInt("PLUTIA_RATE_LIMIT_RESOLVE_BURST", &cfg.RateLimit.ResolveBurst) setFloat64("PLUTIA_RATE_LIMIT_PROOF_RPS", &cfg.RateLimit.ProofRPS) setInt("PLUTIA_RATE_LIMIT_PROOF_BURST", &cfg.RateLimit.ProofBurst) setInt("PLUTIA_HTTP_RETRY_MAX_ATTEMPTS", &cfg.HTTPRetryMaxAttempts) setDuration("PLUTIA_HTTP_RETRY_BASE_DELAY", &cfg.HTTPRetryBaseDelay) setDuration("PLUTIA_HTTP_RETRY_MAX_DELAY", &cfg.HTTPRetryMaxDelay) } func (c Config) Validate() error { if c.Mode != ModeResolver && c.Mode != ModeMirror && c.Mode != ModeThin { return fmt.Errorf("mode must be one of mirror, resolver, or thin (got %q)", c.Mode) } switch c.VerifyPolicy { case VerifyFull, VerifyLazy, VerifyStateOnly: default: return fmt.Errorf("verify must be one of full, lazy, or state-only (got %q)", c.VerifyPolicy) } if c.DataDir == "" { return errors.New("set data_dir in the config file") } if c.PLCSource == "" { return errors.New("set plc_source in the config file") } if c.ZstdLevel < 1 || c.ZstdLevel > 22 { return fmt.Errorf("zstd_level must be between 1 and 22, got %d", c.ZstdLevel) } if c.BlockSizeMB < 4 || c.BlockSizeMB > 16 { return fmt.Errorf("block_size_mb must be between 4 and 16, got %d", c.BlockSizeMB) } if c.CheckpointInterval == 0 { return errors.New("checkpoint_interval must be > 0") } if c.CommitBatchSize <= 0 || c.CommitBatchSize > 4096 { return fmt.Errorf("commit_batch_size must be between 1 and 4096, got %d", c.CommitBatchSize) } if c.VerifyWorkers <= 0 || c.VerifyWorkers > 1024 { return fmt.Errorf("verify_workers must be between 1 and 1024, got %d", c.VerifyWorkers) } if c.ExportPageSize <= 0 || c.ExportPageSize > 1000 { return fmt.Errorf("export_page_size must be between 1 and 1000, got %d", c.ExportPageSize) } if c.ThinCacheTTL <= 0 { return errors.New("thin_cache_ttl must be > 0") } if c.ThinCacheMaxEntries <= 0 { return errors.New("thin_cache_max_entries must be > 0") } if c.ListenAddr == "" { return errors.New("set listen_addr in the config file") } if c.PollInterval <= 0 { return errors.New("poll_interval must be > 0") } if c.RequestTimeout <= 0 { return errors.New("request_timeout must be > 0") } if c.RateLimit.ResolveRPS <= 0 { return errors.New("rate_limit.resolve_rps must be > 0") } if c.RateLimit.ResolveBurst <= 0 { return errors.New("rate_limit.resolve_burst must be > 0") } if c.RateLimit.ProofRPS <= 0 { return errors.New("rate_limit.proof_rps must be > 0") } if c.RateLimit.ProofBurst <= 0 { return errors.New("rate_limit.proof_burst must be > 0") } if c.HTTPRetryMaxAttempts < 1 || c.HTTPRetryMaxAttempts > 32 { return fmt.Errorf("http_retry_max_attempts must be between 1 and 32, got %d", c.HTTPRetryMaxAttempts) } if c.HTTPRetryBaseDelay <= 0 { return errors.New("http_retry_base_delay must be > 0") } if c.HTTPRetryMaxDelay <= 0 { return errors.New("http_retry_max_delay must be > 0") } if c.HTTPRetryBaseDelay > c.HTTPRetryMaxDelay { return errors.New("http_retry_base_delay must be <= http_retry_max_delay") } return nil }