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)
|
||
}
|