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 未返回") } }