Files
杭州明婳科技 96ce883d41 feat: cron/storage/cache/ratelimit 实例化 + trace 纳入 App 生命周期 (v1.4.0)
完成 instance+facade 一致性收尾,闭合三个原始问题:
- cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + SwapDefault* 提升全局默认
  + 包级 facade 代理,照 database.Manager/jwt.Manager 模式),消除单进程多 App 互相污染。
- trace 纳入 App 生命周期(WithTrace + doInit trace.Init + doShutdown trace.Close +
  config.TraceConfig + 自动装 Middleware)。不做实例隔离(OTel 进程全局)。
- 删除 WithoutWire 空函数技术债。

附带修复:
- cache/ratelimit redis 注入(jwt 模型:注入优先、database.GetRedis() 兜底)。
- commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot
  (多 App 下 previous 可能是另一 App 的活跃资源,关闭致 redis: client is closed)。
- app.RateLimitRegistry() + App-bound 限流中间件(多 App per-App 计数隔离)。
- StorageManager.Close() 闭合 Init/Close 对称性(io.Closer 断言,预留扩展点),
  closeResources 与 rollbackReplacedResources 对称收口。

新增公开 API:WithTrace / WithCronTask;app.Scheduler()/Cache()/RedisClient()/
RateLimitRegistry();cron/storage/cache/middleware 各 SwapDefault*;config.TraceConfig;
middleware.RateLimitRegistry + App-bound 方法;middleware.WithRedisClient;
storage.StorageManager.Close。

Breaking:删 WithoutWire;cron.AddTask pre-Init 注册失效(改用 WithCronTask);
middleware.StopRateLimiters 语义收窄;commitReplacedResources 不再关 previous;
cache.NewRedisCacheWithRedis / CacheManager.InitWithRedis 等签名变更。
详见 CHANGELOG [1.4.0] 升级说明。

验证:go build + go vet + go test -race ./... 全绿(-buildvcs=false)。
独立对抗性复审无 CRITICAL/HIGH。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:25:37 +08:00

110 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"strings"
"time"
)
// Validate 校验配置完整性与取值合法性(#16)。
// 在 Manager.Load 解析后自动调用,把"运行时第一次请求才暴露"的配置错误
// 提前到进程启动期。返回的 error 描述具体字段,便于定位。
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("配置为空")
}
var problems []string
// Server
if c.Server.Port < 0 || c.Server.Port > 65535 {
problems = append(problems, fmt.Sprintf("server.port 超出范围(0-65535): %d", c.Server.Port))
}
if c.Server.TLS.Enabled {
if strings.TrimSpace(c.Server.TLS.CertFile) == "" || strings.TrimSpace(c.Server.TLS.KeyFile) == "" {
problems = append(problems, "server.tls 启用后必须同时配置 cert_file 与 key_file")
}
}
if !validDuration(c.Server.ReadTimeout) || !validDuration(c.Server.WriteTimeout) ||
!validDuration(c.Server.IdleTimeout) || !validDuration(c.Server.ShutdownTimeout) {
problems = append(problems, "server 的 timeout 配置不能为负值")
}
// JWT:仅当配置了 secret 时校验(未启用 jwt 的项目可留空)
if c.JWT.Secret != "" {
if len(c.JWT.Secret) < 32 {
problems = append(problems, fmt.Sprintf("jwt.secret 长度不足 32 字节(当前 %d),HMAC 密钥过短不安全", len(c.JWT.Secret)))
}
if c.JWT.Expire < 0 || c.JWT.RefreshExpire < 0 {
problems = append(problems, "jwt.expire / jwt.refresh_expire 不能为负值")
}
}
// Database:出现任一数据库字段时视为启用;driver 为空按 MySQL 兼容处理。
if c.Database.isConfigured() {
driver := strings.TrimSpace(c.Database.Driver)
if driver != "" {
if _, ok := LookupDSNBuilder(driver); !ok {
problems = append(problems, fmt.Sprintf("database.driver 未注册: %s", driver))
}
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && strings.TrimSpace(c.Database.Host) == "" {
problems = append(problems, "database.host 启用数据库后必填")
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && strings.TrimSpace(c.Database.Name) == "" {
problems = append(problems, "database.name 启用数据库后必填")
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && (c.Database.Port <= 0 || c.Database.Port > 65535) {
problems = append(problems, fmt.Sprintf("database.port 超出范围(1-65535): %d", c.Database.Port))
}
// L-config-3:连接池配置交叉校验。MaxOpenConns>0 时 MaxIdleConns 不应超过它,
// 否则 database/sql 会按 MaxOpenConns 截断空闲连接,配置意图与实际不符。
if c.Database.MaxOpenConns > 0 && c.Database.MaxIdleConns > c.Database.MaxOpenConns {
problems = append(problems, fmt.Sprintf(
"database.max_idle_conns(%d) 不应大于 max_open_conns(%d)", c.Database.MaxIdleConns, c.Database.MaxOpenConns))
}
// M-config-2Postgres SSLMode 合法性(非空时校验,空由 PostgresDSN 默认 prefer)。
if sslmode := strings.TrimSpace(c.Database.SSLMode); sslmode != "" {
if !validPostgresSSLMode(sslmode) {
problems = append(problems, fmt.Sprintf(
"database.ssl_mode 非法: %s(允许: disable/allow/prefer/require/verify-ca/verify-full", sslmode))
}
}
// M-config-2TLSRootCA 仅在 TLS=true 时生效,单独配置而无 tls:true 视为配置不一致。
if strings.TrimSpace(c.Database.TLSRootCA) != "" && !c.Database.TLS {
problems = append(problems, "database.tls_root_ca 需配合 tls: true 才生效")
}
}
// Redis:仅当配置了 host 时校验
if strings.TrimSpace(c.Redis.Host) != "" {
if c.Redis.Port <= 0 || c.Redis.Port > 65535 {
problems = append(problems, fmt.Sprintf("redis.port 超出范围(1-65535): %d", c.Redis.Port))
}
}
// Trace:仅当启用时校验采样比例。ExporterType/Propagator 的枚举校验委托 trace.Init
// (随 OTel 版本可能扩展,避免 config 与 trace 双源枚举漂移)。
if c.Trace.Enabled {
if c.Trace.SampleRatio < 0 || c.Trace.SampleRatio > 1 {
problems = append(problems, fmt.Sprintf("trace.sample_ratio 超出范围(0.0-1.0): %v", c.Trace.SampleRatio))
}
}
if len(problems) > 0 {
return fmt.Errorf("配置校验失败: %s", strings.Join(problems, "; "))
}
return nil
}
// validDuration 校验 Duration 非负(0 表示未配置/用默认,合法)。
func validDuration(d time.Duration) bool { return d >= 0 }
// validPostgresSSLMode 校验 PostgreSQL sslmode 取值(M-config-2)。
func validPostgresSSLMode(s string) bool {
switch s {
case "disable", "allow", "prefer", "require", "verify-ca", "verify-full":
return true
}
return false
}