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>
244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
package database
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
func init() {
|
||
// 部分用例触发 logger.Warnf 路径,确保 logger 已初始化避免 nil deref。
|
||
logger.Close()
|
||
}
|
||
|
||
// sentinelDB 返回一个可安全调用 .DB() 的 gorm.DB 占位实例。
|
||
// 直接用 &gorm.DB{} 会令内嵌 *Config 为 nil,.DB() 访问提升字段 ConnPool 时 nil deref;
|
||
// 这里给定非 nil Config,.DB() 走到 nil ConnPool 分支返回 ErrInvalidDB 而不 panic。
|
||
func sentinelDB() *gorm.DB {
|
||
return &gorm.DB{Config: &gorm.Config{}}
|
||
}
|
||
|
||
// TestC11MasterReplicasConcurrentReadWrite 验证 Master/Replicas/Replica 全程加锁,
|
||
// 与并发重建路径(写 master/replicas)无数据竞争(C11d)。
|
||
// 修复前 Master/Replicas 为裸读,-race 必采到竞争。
|
||
func TestC11MasterReplicasConcurrentReadWrite(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
sentinels := []*gorm.DB{{}, {}, {}}
|
||
replicaSets := [][]*gorm.DB{
|
||
{sentinels[0], sentinels[1]},
|
||
{sentinels[2]},
|
||
{},
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
stop := make(chan struct{})
|
||
|
||
// 写者:在锁内置换 master/replicas(模拟 InitDB/InitDBWithReplicas/Close 的写路径)
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
for i := 0; ; i++ {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
default:
|
||
}
|
||
m.mu.Lock()
|
||
m.master = sentinels[i%len(sentinels)]
|
||
m.replicas = replicaSets[i%len(replicaSets)]
|
||
m.mu.Unlock()
|
||
}
|
||
}()
|
||
|
||
// 读者:并发调用读取方法
|
||
for range 4 {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
for {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
default:
|
||
}
|
||
_ = m.Master()
|
||
_ = m.Replicas()
|
||
_ = m.Replica()
|
||
_ = m.FromContext(context.Background())
|
||
}
|
||
}()
|
||
}
|
||
|
||
// 让竞争窗口运行一段时间,确保 -race 能采到(若有未加锁读)
|
||
time.Sleep(50 * time.Millisecond)
|
||
close(stop)
|
||
wg.Wait()
|
||
}
|
||
|
||
// TestC11ReplicasReturnsCopy 验证 Replicas 返回拷贝,调用方修改不影响内部状态(C11d)。
|
||
func TestC11ReplicasReturnsCopy(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
m.mu.Lock()
|
||
m.replicas = []*gorm.DB{{}, {}}
|
||
m.mu.Unlock()
|
||
|
||
got := m.Replicas()
|
||
if len(got) != 2 {
|
||
t.Fatalf("expected 2 replicas, got %d", len(got))
|
||
}
|
||
got[0] = nil
|
||
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.replicas[0] == nil {
|
||
t.Fatal("Replicas should return a copy, not the live slice")
|
||
}
|
||
}
|
||
|
||
// TestC11ReplicaHealthResetOnRebuild 验证重建从库前重置健康状态,
|
||
// 使 initReplicaHealth 按新 replicas 长度重建(C11a)。
|
||
// 修复前 initReplicaHealth 的 replicaHealthSet 早返回导致健康切片与新 replicas 长度错位。
|
||
func TestC11ReplicaHealthResetOnRebuild(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
|
||
// 首轮:2 个从库 + 健康初始化
|
||
m.mu.Lock()
|
||
m.replicas = []*gorm.DB{{}, {}}
|
||
m.mu.Unlock()
|
||
m.initReplicaHealth()
|
||
if !m.replicaHealthSet || len(m.replicaHealthy) != 2 {
|
||
t.Fatalf("expected health init for 2 replicas, got set=%v len=%d", m.replicaHealthSet, len(m.replicaHealthy))
|
||
}
|
||
|
||
// 重建:3 个从库 + 重置健康状态(C11a/C11c 路径)
|
||
m.mu.Lock()
|
||
m.replicas = []*gorm.DB{{}, {}, {}}
|
||
m.resetReplicaHealth()
|
||
m.mu.Unlock()
|
||
m.initReplicaHealth()
|
||
if !m.replicaHealthSet || len(m.replicaHealthy) != 3 {
|
||
t.Fatalf("C11a: expected re-init aligned with 3 new replicas, got set=%v len=%d", m.replicaHealthSet, len(m.replicaHealthy))
|
||
}
|
||
for i := range m.replicaHealthy {
|
||
if !m.replicaHealthy[i].Load() {
|
||
t.Fatal("re-init should mark all replicas healthy")
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestC11ReplicaHealthStaleWithoutReset 反向验证:不调 resetReplicaHealth 时
|
||
// initReplicaHealth 早返回,健康切片长度不随新 replicas 变化(复现 C11a 缺陷根因)。
|
||
func TestC11ReplicaHealthStaleWithoutReset(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
m.mu.Lock()
|
||
m.replicas = []*gorm.DB{{}, {}}
|
||
m.mu.Unlock()
|
||
m.initReplicaHealth()
|
||
|
||
// 仅换 replicas 不重置健康状态
|
||
m.mu.Lock()
|
||
m.replicas = []*gorm.DB{{}, {}, {}}
|
||
m.mu.Unlock()
|
||
m.initReplicaHealth() // replicaHealthSet 仍为 true → 早返回
|
||
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if len(m.replicaHealthy) == 3 {
|
||
t.Fatal("without reset, initReplicaHealth should NOT re-align (early-returns); this proves reset is required")
|
||
}
|
||
if len(m.replicaHealthy) != 2 {
|
||
t.Fatalf("expected stale len=2, got %d", len(m.replicaHealthy))
|
||
}
|
||
}
|
||
|
||
// TestC11ManagerCloseResetsState 验证 Close 清空 master/replicas 并重置健康状态(C11a/C11c/C11d)。
|
||
func TestC11ManagerCloseResetsState(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
m.mu.Lock()
|
||
m.master = sentinelDB()
|
||
m.replicas = []*gorm.DB{sentinelDB(), sentinelDB()}
|
||
m.replicaHealthy = make([]atomic.Bool, 2)
|
||
m.replicaHealthy[0].Store(true)
|
||
m.replicaHealthy[1].Store(true)
|
||
m.replicaHealthSet = true
|
||
m.mu.Unlock()
|
||
m.healthy.Store(true)
|
||
|
||
// closeDB 对空 gorm.DB{}(无 ConnPool)返回 ErrInvalidDB,Close 收集后 join 返回;
|
||
// 这里不关心关闭错误,只断言状态被重置。
|
||
_ = m.Close()
|
||
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.master != nil {
|
||
t.Fatal("expected master nil after Close")
|
||
}
|
||
if m.replicas != nil {
|
||
t.Fatal("expected replicas nil after Close")
|
||
}
|
||
if m.replicaHealthSet {
|
||
t.Fatal("expected replicaHealthSet reset after Close")
|
||
}
|
||
if m.replicaHealthy != nil {
|
||
t.Fatal("expected replicaHealthy nil after Close")
|
||
}
|
||
if m.healthy.Load() {
|
||
t.Fatal("expected healthy=false after Close")
|
||
}
|
||
}
|
||
|
||
// TestC11PackageCloseClosesReplicas 验证包级 Close 委托 CloseAll,
|
||
// 关闭从库而非仅主库(C11f)。修复前包级 Close 仅关 master,replicas 残留。
|
||
func TestC11PackageCloseClosesReplicas(t *testing.T) {
|
||
// 保存并恢复 DefaultManager 状态,避免污染其他用例
|
||
defer func() {
|
||
DefaultManager.Load().mu.Lock()
|
||
DefaultManager.Load().master = nil
|
||
DefaultManager.Load().replicas = nil
|
||
DefaultManager.Load().resetReplicaHealth()
|
||
DefaultManager.Load().healthy.Store(false)
|
||
DefaultManager.Load().mu.Unlock()
|
||
}()
|
||
|
||
DefaultManager.Load().mu.Lock()
|
||
DefaultManager.Load().master = sentinelDB()
|
||
DefaultManager.Load().replicas = []*gorm.DB{sentinelDB(), sentinelDB()}
|
||
DefaultManager.Load().mu.Unlock()
|
||
|
||
// 包级 Close → CloseAll → DefaultManager.Load().Close(),应同时清空 master 与 replicas
|
||
_ = Close()
|
||
|
||
DefaultManager.Load().mu.Lock()
|
||
master := DefaultManager.Load().master
|
||
repl := DefaultManager.Load().replicas
|
||
DefaultManager.Load().mu.Unlock()
|
||
|
||
if master != nil {
|
||
t.Fatal("C11f: expected master nil after package Close")
|
||
}
|
||
if repl != nil {
|
||
t.Fatal("C11f: package Close should close replicas too (delegates to CloseAll), got residual replicas")
|
||
}
|
||
}
|
||
|
||
// TestC11HealthCheckLockedRead 验证 HealthCheck 在锁内快照 master,未初始化时返错(C11d)。
|
||
func TestC11HealthCheckLockedRead(t *testing.T) {
|
||
m := &Manager{picker: &RandomPicker{}}
|
||
if err := m.HealthCheck(context.Background()); err == nil {
|
||
t.Fatal("expected error when health checking uninitialized master")
|
||
}
|
||
|
||
// 空 gorm.DB(无 ConnPool):DB() 返 ErrInvalidDB,HealthCheck 返该错而非 panic
|
||
m.mu.Lock()
|
||
m.master = sentinelDB()
|
||
m.mu.Unlock()
|
||
if err := m.HealthCheck(context.Background()); err == nil {
|
||
t.Fatal("expected error for gorm.DB without ConnPool")
|
||
}
|
||
}
|