6595e94677
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见 last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。 go build / vet / test -race ./... 全绿。 安全(P0): - JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言 - JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm) - 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size - HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control) 并发/资源(主线A 闭合): - 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage / DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争 - trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown - ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron) - ratelimit failClosed atomic.Bool;Recover Written() 守卫 - cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态) 终审剩余项收口(H-A~M-H + L 系列): - 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns) - JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用 - GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU - config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady - compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold - 删 redisLimiters 死代码;CLI 输入校验 + 回滚 文档: - README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明) - docs/README 索引修正;config 注释修正(RS256 已不支持) 破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package database
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// RedisManager Redis 连接管理器(#10)。照 database.Manager 模式:
|
||
// 实例化 + DefaultRedis 全局默认 + 包级 facade 代理,支持多实例与测试注入。
|
||
type RedisManager struct {
|
||
mu sync.Mutex
|
||
cfg *config.Config
|
||
client *redis.Client
|
||
}
|
||
|
||
// DefaultRedis 默认 Redis 管理器,包级 facade 代理到它。
|
||
//
|
||
// C-1/H-4 修复:改用 atomic.Pointer 保护读写,消除原裸指针置换(SetDefaultRedisManager)
|
||
// 与请求 goroutine 无锁读(GetRedis 等 facade)之间的数据竞争。与 config.defaultManager
|
||
// 对齐。类型由 *RedisManager 变更为 atomic.Pointer[RedisManager](breaking:下游若直接
|
||
// 调用 DefaultRedis.Init 等方法需改用 InitRedis 等 facade,或 DefaultRedis.Load().Init)。
|
||
var DefaultRedis atomic.Pointer[RedisManager]
|
||
|
||
func init() {
|
||
DefaultRedis.Store(NewRedisManager())
|
||
}
|
||
|
||
// NewRedisManager 创建 Redis 管理器实例。
|
||
func NewRedisManager() *RedisManager { return &RedisManager{} }
|
||
|
||
// SetDefaultRedisManager 提升指定 RedisManager 为全局默认,后续包级 facade 走它。
|
||
// 用于多实例场景或测试注入 mock。并发安全(atomic.Store)。
|
||
func SetDefaultRedisManager(m *RedisManager) {
|
||
if m != nil {
|
||
DefaultRedis.Store(m)
|
||
}
|
||
}
|
||
|
||
// Init 初始化 Redis 连接并 ping 验证。
|
||
func (m *RedisManager) Init(cfg *config.Config) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
client := redis.NewClient(&redis.Options{
|
||
Addr: cfg.Redis.Addr(),
|
||
Password: cfg.Redis.Password,
|
||
DB: cfg.Redis.DB,
|
||
DialTimeout: 5 * time.Second, // D7 修复:连接超时
|
||
ReadTimeout: 3 * time.Second, // D7 修复:读超时
|
||
WriteTimeout: 3 * time.Second, // D7 修复:写超时
|
||
})
|
||
|
||
pingCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if err := client.Ping(pingCtx).Err(); err != nil {
|
||
client.Close()
|
||
return fmt.Errorf("Redis 连接失败: %w", err)
|
||
}
|
||
|
||
m.cfg = cfg
|
||
m.client = client
|
||
logger.Info("Redis 连接成功", zap.String("addr", cfg.Redis.Addr()))
|
||
return nil
|
||
}
|
||
|
||
// Close 关闭 Redis 连接。
|
||
func (m *RedisManager) Close() error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
if m.client == nil {
|
||
return nil
|
||
}
|
||
err := m.client.Close()
|
||
m.client = nil
|
||
return err
|
||
}
|
||
|
||
// Client 返回当前 Redis 客户端(未初始化返回 nil)。
|
||
func (m *RedisManager) Client() *redis.Client {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
return m.client
|
||
}
|
||
|
||
// setClientForTest 仅测试用:在持锁下替换 manager 的 client,返回旧 client。
|
||
// 供 SetTestRedisClient 注入 miniredis 等 mock,消除原包级 redisClient 双源真相。
|
||
func (m *RedisManager) setClientForTest(c *redis.Client) *redis.Client {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
old := m.client
|
||
m.client = c
|
||
return old
|
||
}
|
||
|
||
// HealthCheck Redis 健康检查。
|
||
func (m *RedisManager) HealthCheck(ctx context.Context) error {
|
||
m.mu.Lock()
|
||
client := m.client
|
||
m.mu.Unlock()
|
||
if client == nil {
|
||
return fmt.Errorf("Redis 未初始化")
|
||
}
|
||
return client.Ping(ctx).Err()
|
||
}
|
||
|
||
// --- 包级 facade(代理到 DefaultRedis,兼容存量) ---
|
||
|
||
// InitRedis 初始化 Redis 连接
|
||
func InitRedis(cfg *config.Config) error {
|
||
return DefaultRedis.Load().Init(cfg)
|
||
}
|
||
|
||
// CloseRedis 关闭 Redis 连接
|
||
func CloseRedis() error {
|
||
return DefaultRedis.Load().Close()
|
||
}
|
||
|
||
// HealthCheckRedis Redis 健康检查
|
||
func HealthCheckRedis(ctx context.Context) error {
|
||
return DefaultRedis.Load().HealthCheck(ctx)
|
||
}
|
||
|
||
// GetRedis 获取 Redis 客户端(未初始化返回 nil)。
|
||
//
|
||
// H-4 修复:单源——仅从 DefaultRedis.Load().Client() 取,废弃原包级 redisClient
|
||
// 回退路径(其在 manager 替换后会返回 stale client,且无锁读存在竞态)。
|
||
// 测试注入的 mock client 经 SetTestRedisClient 直接设在当前 manager 上,闭环一致。
|
||
func GetRedis() *redis.Client {
|
||
return DefaultRedis.Load().Client()
|
||
}
|
||
|
||
// SetTestRedisClient 供测试注入 miniredis 等 mock 客户端。
|
||
// 返回旧客户端引用以便测试清理时恢复。生产代码严禁调用。
|
||
//
|
||
// H-4 修复:改为在当前默认 manager 上持锁替换 client(setClientForTest),
|
||
// 不再写独立的包级 redisClient 变量,消除双源真相与无锁写竞态。
|
||
//
|
||
// 约束:操作 DefaultRedis.Load() 返回的当前默认 manager。测试应避免在调用本函数
|
||
// 的同时并发 SetDefaultRedisManager 替换默认 manager,否则注入/恢复会作用到不同
|
||
// manager 上。典型用法为 init 阶段注入、t.Cleanup 恢复,串行执行。
|
||
func SetTestRedisClient(c *redis.Client) *redis.Client {
|
||
return DefaultRedis.Load().setClientForTest(c)
|
||
}
|