项目文件夹

文件
杭州明婳科技 6730f12cd6 test(database): H-db-1 补齐行为闭环 + 修复 gorm.Open automatic ping
上一提交(72ea01d)修了 4 条 pingWithTimeout 路径,但行为验证有缺口:
启动 ping 路径仅代码审查、Redis HealthCheck 仅推断、无 App 级端到端。
补齐过程发现并修复了 H-db-1 的真正根因之一:

gorm.Open 的 automatic ping(gorm.go:204,!DisableAutomaticPing 且 ConnPool
为 *sql.DB 时调 pinger.Ping() 无超时)在 pingWithTimeout 之前就无限阻塞。
挂起 DB 下 initDB 的 gorm.Open 永久 hang,根本到不了 pingWithTimeout。
修复:initDB/InitDBWithReplicas 的 gormConfig 加 DisableAutomaticPing: true,
框架用 pingWithTimeout(3s)自管启动 ping。

行为闭环测试(3 缺口补齐):
- manager_hdb1_internal_test.go:新增 TestInitDBBoundsHungDB_Hdb1 /
  TestInitDBWithReplicasBoundsHungDB_Hdb1(hungDialector 注入挂起 *sql.DB,
  回归启动 master/replica ping 路径有界)。原 3 用例保留。
- redis_hdb1_internal_test.go:startHungRedisListener(accept 不响应模拟挂起
  Redis)+ TestRedisHealthCheckBoundsHungRedis_Hdb1,行为验证 HealthCheck
  受 client ReadTimeout 3s 约束(不再是推断)。提取 newRedisClient helper
  供 Init 与测试共用。
- app_hdb1_test.go:TestAppInitHungDBBounded_Hdb1 App 级端到端--App.Init
  挂起 DB 有界失败(~30s 5 次重试,非无限 hang)+ Shutdown 正常退出。

验证:database -race -short green(24.7s,6 个 H-db-1 用例)、root 包 App
-race green(33.5s,含端到端)、build/vet 干净。CHANGELOG 更新。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 00:52:17 +08:00

85 行
2.9 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package database
import (
"context"
"net"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
// startHungRedisListener 启动一个接受 TCP 连接但不读写任何数据的 listener,
// 模拟"挂起 Redis"(连接建立但不响应 RESP,区别于宕机的 connection-refused)。
// 返回 listener 地址;t.Cleanup 关闭 listener。
func startHungRedisListener(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("hung redis listen: %v", err)
}
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return // listener 关闭
}
// 故意不读写、不关闭,持住连接模拟挂起。连接由 t.Cleanup 的 ln.Close 间接清理。
_ = conn
}
}()
return ln.Addr().String()
}
// TestRedisHealthCheckBoundsHungRedis_Hdb1 回归 H-db-1 边界(Redis 侧):
// RedisManager.HealthCheck(Background) 对挂起 Redis(连接活但不响应)应受
// redis client 的 ReadTimeout(3s,redis.go D7) 约束有界返回错误,不无限阻塞。
// ctx 无 deadline 时由 client ReadTimeout 兜底;ctx 自带更短 deadline 时优先尊重 ctx。
// 修复前/缺 ReadTimeout 时 Ping(Background) 会无限阻塞。
func TestRedisHealthCheckBoundsHungRedis_Hdb1(t *testing.T) {
addr := startHungRedisListener(t)
cfg := redisTestConfig(t, addr)
m := NewRedisManager()
if err := m.Init(cfg); err == nil {
// Init 的 Ping 有 5s ctx,挂起 Redis 下应在 ~3sReadTimeout)失败返回错误。
t.Fatalf("挂起 Redis 的 Init 应返回错误,got nil")
}
// Init 失败后 m.client 未被安装(nil),HealthCheck 返 "Redis 未初始化"。
// 为测 HealthCheck 自身的 ping 超时约束,需 client 已安装但指向挂起 Redis。
// 用 setClientForTest 注入一个指向挂起 Redis 的 client。
client := newRedisClientForTest(addr)
old := m.setClientForTest(client)
t.Cleanup(func() {
if old != nil {
_ = old.Close()
}
_ = client.Close()
})
// HealthCheck(Background):挂起 Redis 下 client.Ping 受 ReadTimeout 3s 约束有界返回。
done := make(chan error, 1)
start := time.Now()
go func() { done <- m.HealthCheck(context.Background()) }()
select {
case err := <-done:
elapsed := time.Since(start)
if err == nil {
t.Fatalf("挂起 Redis 的 HealthCheck 应返回错误,got nil")
}
// 应在 ReadTimeout(3s) 附近返回,给 6s 上限(含连接/重试余量)。
if elapsed > 6*time.Second {
t.Fatalf("HealthCheck 耗时 %v 超过 6s 上限(应受 ReadTimeout 3s 约束)", elapsed)
}
case <-time.After(10 * time.Second):
t.Fatalf("HealthCheck 在挂起 Redis 上无限阻塞(ReadTimeout 未约束 Ping")
}
}
// newRedisClientForTest 构造指向 addr 的 redis.Client(复用 Init 的 client 配置,
// 含 DialTimeout 5s / ReadTimeout 3s / WriteTimeout 3s)。
func newRedisClientForTest(addr string) *redis.Client {
return newRedisClient(addr, "", 0)
}