feat(qq-obv11): client 模式首连失败不再致命(复用既有重连循环 + 指数退避)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

痛点: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/**,本切片禁碰)。
This commit is contained in:
AskaEth committed 2026-10-05 10:12:23 +08:00
1 parent d804c4e675
commit 8c437866e7
6 files changed
+698 -10

No files matched your search

+13 -7
View File
@@ -114,20 +114,26 @@ func (a *Adapter) Addr() string {
}
// readLoop is the single inbound-reader goroutine. It reads frames from the
// active connection, dispatching message events; in client mode it reconnects
// after drops, in server mode it waits for a new OneBot client.
// active connection, dispatching message events; in client mode it connects and
// reconnects after drops (exponential backoff, see reconnect.go), in server mode
// it waits for a new OneBot client.
func (a *Adapter) readLoop(ctx context.Context) {
for {
conn := a.currentConn()
if conn == nil {
if a.cfg.Mode == "client" {
if a.cfg.Mode == modeClient {
// ws+client:首连失败与断线重连**共用这一条路径**——Connect 首连失败
// 只是记一条 WARN 就返回(不致命),真正的重试在这里按退避进行。
// 先等待、再拨号:日志里承诺的 retryIn 就是这次等待。等待结束后再查一次
// ctx:与 Disconnect 赛跑时宁可少拨一次,也不发起注定失败的拨号。
if !sleepCtx(ctx, a.reconnect.wait(a.policy)) || ctx.Err() != nil {
return
}
if err := a.connectClient(ctx); err != nil {
a.log.Warn("onebot reconnect failed", "err", err)
if !sleepCtx(ctx, 3*time.Second) {
return
}
a.noteConnectFailure(err)
continue
}
a.reconnect.reset()
} else if !a.waitConn(ctx) {
return
}