131 lines
5.2 KiB
Go
131 lines
5.2 KiB
Go
package qq_obv11
|
||
|
||
// 本文件实现 **ws + client**(Cyrene 主动拨号 OneBot 正向 WS)的首连/重连退避策略。
|
||
//
|
||
// 为什么需要它:NapCat 等 OneBot 实现可能比 Cyrene 后启动(或中途重启),若首连失败
|
||
// 就判定渠道起不来,「谁先起来」就成了部署顺序的硬约束。因此首连失败必须非致命:
|
||
// 交给后台重试循环按指数退避继续拨号——退避带抖动(多实例同时重连时错开)且有上限
|
||
// (对面长期不在线时按固定上限间隔探测,不做无界增长)。连接状态始终以真实连接为准:
|
||
// 没连上就是 connected=false,绝不假装已连(面板与 HealthCheck 都照此判断)。
|
||
//
|
||
// 设计依据:DevDocs/design/08-渠道接入.md §4 与 AGENTS §9——client 模式不开监听端口,
|
||
// 重试只发生在出站拨号上。
|
||
//
|
||
// 既有实现对照(AGENTS §9.5):项目已有 `internal/channel/adapter/wsutil.Backoff`
|
||
// (2s 起步、×2、60s 封顶的网关重连调度器,KOOK 用;design/08 §7「WebSocket 有状态
|
||
// channel 的公共基座」计划让各 WS 平台共用这套重连基座)。本包**口径与之对齐**(同样
|
||
// 2s/60s),但不直接 import 它,理由有三:
|
||
// ① 本需求要 ±抖动,wsutil.Backoff 没有这个能力;② import wsutil 会把 gorilla/websocket
|
||
// 拖进本包依赖图,而本包刻意只用标准库携带 WS 帧编解码(见 websocket.go 抬头);
|
||
// ③ 真正收敛的做法是给 wsutil.Backoff 加可选抖动再迁走,那要改 internal/**,属后续工作。
|
||
|
||
import (
|
||
"math/rand/v2"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
// reconnectBaseDelay 是重试等待时长的初值(第一次失败后按因子翻倍)。取值与
|
||
// wsutil.Backoff 的网关重连口径一致(2s 起步、60s 封顶)。
|
||
reconnectBaseDelay = 2 * time.Second
|
||
// reconnectMaxDelay 是退避上限:到顶后按固定间隔继续探测,不再增长。
|
||
reconnectMaxDelay = 60 * time.Second
|
||
// reconnectBackoffFactor 是每多一次连续失败,等待时长的放大倍数。
|
||
reconnectBackoffFactor = 2
|
||
// reconnectMaxDoublings 限制翻倍次数,避免移位/乘法溢出(结果随后仍被上限裁剪)。
|
||
reconnectMaxDoublings = 6
|
||
// reconnectJitterRatio 是抖动幅度(±25%),让多实例的重连打点错开。
|
||
reconnectJitterRatio = 0.25
|
||
)
|
||
|
||
// reconnectPolicy 是退避参数的可注入载体:生产用 defaultReconnectPolicy()(由上面的
|
||
// 具名常量构造),测试替换成毫秒级参数即可确定性验证重试,不必真等秒级退避。
|
||
type reconnectPolicy struct {
|
||
baseDelay time.Duration
|
||
maxDelay time.Duration
|
||
factor int
|
||
maxDoublings int
|
||
jitterRatio float64
|
||
}
|
||
|
||
// defaultReconnectPolicy 返回生产退避参数。
|
||
func defaultReconnectPolicy() reconnectPolicy {
|
||
return reconnectPolicy{
|
||
baseDelay: reconnectBaseDelay,
|
||
maxDelay: reconnectMaxDelay,
|
||
factor: reconnectBackoffFactor,
|
||
maxDoublings: reconnectMaxDoublings,
|
||
jitterRatio: reconnectJitterRatio,
|
||
}
|
||
}
|
||
|
||
// delayFor 返回「已连续失败 failures 次」之后应等待的时长:
|
||
// baseDelay × factor^failures,叠加 ±jitterRatio 抖动后裁剪到 [0, maxDelay]。
|
||
// 指数项先按 maxDoublings 收敛再来抖动、最后裁上限,因此退避有界、绝不无限增长。
|
||
func (p reconnectPolicy) delayFor(failures int) time.Duration {
|
||
steps := failures
|
||
if steps < 0 {
|
||
steps = 0
|
||
}
|
||
if steps > p.maxDoublings {
|
||
steps = p.maxDoublings
|
||
}
|
||
d := p.baseDelay
|
||
for i := 0; i < steps; i++ {
|
||
d *= time.Duration(p.factor)
|
||
}
|
||
if p.jitterRatio > 0 {
|
||
d = time.Duration(float64(d) * (1 + jitterOffset(p.jitterRatio)))
|
||
}
|
||
if d > p.maxDelay {
|
||
d = p.maxDelay
|
||
}
|
||
if d < 0 {
|
||
d = 0
|
||
}
|
||
return d
|
||
}
|
||
|
||
// jitterOffset 返回 [-ratio, +ratio] 上的均匀随机偏移。
|
||
func jitterOffset(ratio float64) float64 {
|
||
return (rand.Float64()*2 - 1) * ratio
|
||
}
|
||
|
||
// reconnectState 是 client 模式的连续失败计数器。Connect 的首连失败与 readLoop 的
|
||
// 重连失败共享同一份状态,因此退避在一轮启动内连续;成功连上即清零。
|
||
type reconnectState struct {
|
||
mu sync.Mutex
|
||
failures int
|
||
}
|
||
|
||
// fail 记一次失败并返回下次重试前应等待的时长(该值就是要写进日志的 retryIn)。
|
||
func (s *reconnectState) fail(p reconnectPolicy) time.Duration {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.failures++
|
||
return p.delayFor(s.failures)
|
||
}
|
||
|
||
// wait 返回当前应等待的时长(不推进计数):与上一次失败记录的 retryIn 一致,
|
||
// 保证日志里承诺的间隔就是下一次真正拨号前的等待。
|
||
func (s *reconnectState) wait(p reconnectPolicy) time.Duration {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return p.delayFor(s.failures)
|
||
}
|
||
|
||
// reset 清零连续失败计数(成功连上或重新 Connect 时调用)。
|
||
func (s *reconnectState) reset() {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.failures = 0
|
||
}
|
||
|
||
// noteConnectFailure 记录一次 client 模式的连接失败:推进退避,并写一条带来源的
|
||
// WARN(含 err 与下次重试间隔)。access_token 绝不入日志。
|
||
func (a *Adapter) noteConnectFailure(err error) {
|
||
delay := a.reconnect.fail(a.policy)
|
||
a.log.Warn("onebot connect failed; retrying", "err", err, "retryIn", delay)
|
||
}
|