痛点:mode=client 时首次拨号失败会让 Connect 直接返回 error ⇒ 渠道起不来; 而「连上后再断」本来就有重连。⇒ 把 readLoop 的入口条件放宽为「conn==nil 也算」, **不新造第二套循环**。 新增 reconnect.go:退避 2s → ×2 → 上限 60s、抖动 ±25%(具名常量),失败计数跨 Connect 与 readLoop 共享。 ws_client:新增 connectFirstClient —— 可重试失败只记 WARN 并返回 nil; 配置缺失(remote_url 空)仍致命(闸门口径不变)。 connected 语义不许撒谎:始终由真实连接决定;HealthCheck 未连上返回 ErrNotConnected; 仅 http 传输保留「已装配即可服务」旧口径(逐字未改)。 ctx 感知等待(sleepCtx)+ 等待后复查 ctx ⇒ Disconnect 能立即中止,不泄漏 goroutine。 server 模式与 http 三端行为逐字不变(既有用例零改动全绿 + TestServerModeUnaffectedByClientRetry)。 测试:client_retry_test.go(428) + reconnect_test.go(83),用可注入 dialer + gate 通道造确定性时序。 变异 5 条(首连 fail-fast / 退避无上限 / 取消失效 / 未连上就报已连 / 抖动未接线)。 诚实边界:60s 上限对聊天渠道可能偏慢(改 30s 是一行常量);仓库内仍存第二份指数退避 (wsutil.Backoff 无抖动能力,收敛需改 internal/**,本切片禁碰)。
84 lines
2.6 KiB
Go
84 lines
2.6 KiB
Go
package qq_obv11
|
|
|
|
// 本文件只测退避策略本身(纯函数),不碰连接:重试循环的行为断言见
|
|
// client_retry_test.go。
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestReconnectBackoffIsBounded 断言退避**有上限**:无论连续失败多少次,
|
|
// 等待时长都不会超过 reconnectMaxDelay(对面的长期离线不能让等待无限增长)。
|
|
func TestReconnectBackoffIsBounded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := defaultReconnectPolicy()
|
|
for failures := 0; failures <= 50; failures++ {
|
|
got := p.delayFor(failures)
|
|
if got <= 0 {
|
|
t.Fatalf("delayFor(%d) = %v, want > 0", failures, got)
|
|
}
|
|
if got > reconnectMaxDelay {
|
|
t.Fatalf("delayFor(%d) = %v, 超过上限 %v(退避无上限)", failures, got, reconnectMaxDelay)
|
|
}
|
|
}
|
|
// 初值附近:抖动最多 ±reconnectJitterRatio。
|
|
base := p.delayFor(0)
|
|
if base < time.Duration(float64(reconnectBaseDelay)*(1-reconnectJitterRatio)) ||
|
|
base > time.Duration(float64(reconnectBaseDelay)*(1+reconnectJitterRatio)) {
|
|
t.Errorf("delayFor(0) = %v, 期望落在初值 %v ±%v 内", base, reconnectBaseDelay, reconnectJitterRatio)
|
|
}
|
|
}
|
|
|
|
// TestReconnectBackoffGrowsThenCaps 断言退避按因子增长、到顶后稳住(把抖动关掉,
|
|
// 取值才是确定的)。
|
|
func TestReconnectBackoffGrowsThenCaps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := defaultReconnectPolicy()
|
|
p.jitterRatio = 0
|
|
|
|
if got := p.delayFor(0); got != reconnectBaseDelay {
|
|
t.Errorf("delayFor(0) = %v, want %v", got, reconnectBaseDelay)
|
|
}
|
|
for _, tc := range []struct {
|
|
failures int
|
|
want time.Duration
|
|
}{
|
|
{1, 4 * time.Second},
|
|
{2, 8 * time.Second},
|
|
{3, 16 * time.Second},
|
|
{4, 32 * time.Second},
|
|
{5, reconnectMaxDelay}, // 64s 被上限裁到 60s
|
|
{20, reconnectMaxDelay}, // 远超上限仍是上限
|
|
} {
|
|
if got := p.delayFor(tc.failures); got != tc.want {
|
|
t.Errorf("delayFor(%d) = %v, want %v", tc.failures, got, tc.want)
|
|
}
|
|
}
|
|
prev := time.Duration(0)
|
|
for failures := 0; failures <= 10; failures++ {
|
|
got := p.delayFor(failures)
|
|
if got < prev {
|
|
t.Fatalf("delayFor(%d) = %v < 前一次 %v(退避不单调)", failures, got, prev)
|
|
}
|
|
prev = got
|
|
}
|
|
}
|
|
|
|
// TestReconnectBackoffJitters 断言抖动确实接了线:同一失败次数多次取值不全相等
|
|
// (多实例同时重连时打点才会错开)。
|
|
func TestReconnectBackoffJitters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := defaultReconnectPolicy()
|
|
seen := map[time.Duration]struct{}{}
|
|
for i := 0; i < 64; i++ {
|
|
seen[p.delayFor(3)] = struct{}{}
|
|
}
|
|
if len(seen) < 2 {
|
|
t.Errorf("delayFor(3) 反复取值只有 %d 种,抖动未生效", len(seen))
|
|
}
|
|
}
|