Files
Cyrene_v2/plugins/channels/qq_obv11/reconnect_test.go
T
AskaEth 8c437866e7
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(qq-obv11): client 模式首连失败不再致命(复用既有重连循环 + 指数退避)
痛点: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/**,本切片禁碰)。
2026-10-05 10:12:23 +08:00

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