项目文件夹

文件
2026-07-14 10:24:10 +08:00

405 行
12 KiB
Go

package database
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/EthanCodeCraft/xlgo-core/config"
"github.com/EthanCodeCraft/xlgo-core/logger"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)
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")
}
}
}
func TestM4ProbeOnceReinitializesReplicaHealthAfterRebuild(t *testing.T) {
m := &Manager{picker: &RandomPicker{}}
m.mu.Lock()
m.replicas = []*gorm.DB{sentinelDB(), sentinelDB()}
m.resetReplicaHealth()
m.mu.Unlock()
m.probeOnce(context.Background(), 1)
m.mu.Lock()
defer m.mu.Unlock()
if !m.replicaHealthSet {
t.Fatal("probeOnce 应在从库重建后自动重建健康标记")
}
if len(m.replicaHealthy) != 2 {
t.Fatalf("健康标记长度应与从库数量一致,实际 %d", len(m.replicaHealthy))
}
}
// 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")
}
}
func TestSetDefaultManagerClosesReplacedManager(t *testing.T) {
old := NewManager(nil)
old.mu.Lock()
old.master = sentinelDB()
old.replicas = []*gorm.DB{sentinelDB()}
old.replicaHealthy = make([]atomic.Bool, 1)
old.replicaHealthy[0].Store(true)
old.replicaHealthSet = true
old.mu.Unlock()
old.healthy.Store(true)
orig := SwapDefaultManager(old)
t.Cleanup(func() {
current := SwapDefaultManager(orig)
if current != nil && current != orig {
_ = current.Close()
}
})
next := NewManager(nil)
SetDefaultManager(next)
if GetDefaultManager() != next {
t.Fatal("SetDefaultManager 应安装新的默认 manager")
}
old.mu.Lock()
defer old.mu.Unlock()
if old.master != nil || old.replicas != nil || old.replicaHealthy != nil || old.replicaHealthSet {
t.Fatal("SetDefaultManager 应关闭并清空被替换的旧 manager,避免连接池泄漏")
}
if old.healthy.Load() {
t.Fatal("SetDefaultManager 关闭旧 manager 后健康状态应为 false")
}
}
func TestSwapDefaultManagerPreservesReplacedManager(t *testing.T) {
old := NewManager(nil)
old.mu.Lock()
old.master = sentinelDB()
old.replicas = []*gorm.DB{sentinelDB()}
old.mu.Unlock()
orig := SwapDefaultManager(old)
t.Cleanup(func() {
current := SwapDefaultManager(orig)
if current != nil && current != orig {
_ = current.Close()
}
_ = old.Close()
})
next := NewManager(nil)
previous := SwapDefaultManager(next)
if previous != old {
t.Fatal("SwapDefaultManager 应返回被替换的旧 manager")
}
if GetDefaultManager() != next {
t.Fatal("SwapDefaultManager 应安装新的默认 manager")
}
old.mu.Lock()
defer old.mu.Unlock()
if old.master == nil || len(old.replicas) != 1 {
t.Fatal("SwapDefaultManager 不应关闭旧 manager,旧资源需可用于失败回滚")
}
}
type transientDialector struct{}
func (d transientDialector) Name() string { return "m4_transient" }
func (d transientDialector) Initialize(*gorm.DB) error {
return errors.New("temporary connection failure")
}
func (d transientDialector) Migrator(*gorm.DB) gorm.Migrator { return nil }
func (d transientDialector) DataTypeOf(*schema.Field) string { return "" }
func (d transientDialector) DefaultValueOf(*schema.Field) clause.Expression { return nil }
func (d transientDialector) BindVarTo(clause.Writer, *gorm.Statement, any) {}
func (d transientDialector) QuoteTo(writer clause.Writer, str string) {
_, _ = writer.WriteString(str)
}
func (d transientDialector) Explain(sql string, _ ...any) string { return sql }
func TestM4InitDBHonorsContextDuringRetrySleep(t *testing.T) {
const driver = "m4_transient_retry"
RegisterDialect(DialectSpec{
Name: driver,
Dialector: func(string) gorm.Dialector { return transientDialector{} },
DSN: func(*config.DatabaseConfig) string { return "m4://retry" },
})
m := NewManager(nil)
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(30 * time.Millisecond)
cancel()
}()
start := time.Now()
err := m.InitDB(ctx, &config.Config{Database: config.DatabaseConfig{Driver: driver}})
if err == nil || !errors.Is(err, context.Canceled) {
t.Fatalf("InitDB 应返回 context.Canceled,实际: %v", err)
}
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
t.Fatalf("InitDB 不应等待完整 retry sleep,耗时 %s", elapsed)
}
}
func TestM4CloseWaitsForLifecycleOperation(t *testing.T) {
m := NewManager(nil)
m.opMu.Lock()
done := make(chan struct{})
go func() {
_ = m.Close()
close(done)
}()
select {
case <-done:
t.Fatal("Close 不应越过正在执行的生命周期操作")
case <-time.After(50 * time.Millisecond):
}
m.opMu.Unlock()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("释放生命周期锁后 Close 未返回")
}
}