6730f12cd6
上一提交(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>
85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
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 下应在 ~3s(ReadTimeout)失败返回错误。
|
||
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)
|
||
}
|