feat(phase2e-2): WS 稳定性改进——重连后自动重绑定房间 + 多端登录抖动检测
背景 - Task 9 联调发现同账号多 tab 登录会触发后端单设备踢下线机制,WS 每秒被关闭一次, 所有 meeting.* 的 sendWithAck 都因 "连接已断开" 而失败,表现为"开启音视频提示失败" - 同时 WS 断线重连后后端对该连接的 roomCode 绑定丢失,会议广播不再送达本端 改动 - store/meeting.js:_registerListeners 增加 WS 生命周期监听 - _disconnected:从 CONNECTED/CONNECTING 切到 RECONNECTING,UI 可展示"重连中…" - _connected:在 RECONNECTING 状态自动重发 meeting.room.join 重绑定房间,并刷新参与者列表 - services/websocket.js:_recordCloseForFlapping 做 10s 滑窗 flapping 检测 - 窗口内累计 >=3 次非主动断连 → 触发 _flapping 事件(含 count/windowMs) - 15s 冷却避免抖动期间重复告警 - debug.vue:新增 WS 状态指示灯(已连接/重连中/已断)+ flapping 警告横幅 - 订阅 _connected / _disconnected / _flapping 三个 WS 内置事件,onBeforeUnmount 注销 验证(Playwright) - 单次断连:testuser1 创建会议后手动 close WS → 指示灯 reconnecting,1s 后重连 - 日志链 "WS 已断开 → 进入 reconnecting → 连接成功 → 重绑定房间 307-857-909 → 恢复 connected" 全通 - 4 次连续断连:第 3 次触发 flapping,UI 横幅 "⚠ 检测到异常重连(3 次/10s),可能同账号在其他端登录" - 每次断连后房号、主持人状态、localState 均正确恢复为 connected Made-with: Cursor
This commit is contained in:
@@ -19,6 +19,11 @@ const RECONNECT_BASE_DELAY = 1000
|
||||
const RECONNECT_MAX_DELAY = 30000
|
||||
const MAX_RECONNECT_ATTEMPTS = Infinity
|
||||
const ACK_DEFAULT_TIMEOUT_MS = 10000
|
||||
/** 异常重连检测:10s 窗口内 >=3 次断连即认为异常抖动(常见于同账号多 tab 互踢) */
|
||||
const FLAPPING_WINDOW_MS = 10000
|
||||
const FLAPPING_THRESHOLD = 3
|
||||
/** 两次 flapping 警告之间最小间隔,避免持续抖动时每秒触发 */
|
||||
const FLAPPING_NOTIFY_COOLDOWN_MS = 15000
|
||||
|
||||
class WebSocketService {
|
||||
constructor() {
|
||||
@@ -34,6 +39,10 @@ class WebSocketService {
|
||||
* Task 9 引入,为 meeting.* 事件提供 Promise 化 ACK 等待
|
||||
*/
|
||||
this.pendingAcks = new Map()
|
||||
/** 近期断连时间戳数组,用于 flapping 检测 */
|
||||
this.recentCloseAt = []
|
||||
/** 上次 flapping 警告时间,用于冷却 */
|
||||
this.lastFlappingNotifyAt = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,13 +222,31 @@ class WebSocketService {
|
||||
console.log('[WS] 连接关闭', e)
|
||||
this._clearTimers()
|
||||
this._rejectAllPendingAcks('连接已断开')
|
||||
this._emit('_disconnected')
|
||||
this._emit('_disconnected', e)
|
||||
|
||||
if (!this.isManualClose) {
|
||||
this._recordCloseForFlapping()
|
||||
this._scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次非主动断连,并在短窗口内多次断连时触发 _flapping 事件
|
||||
* 典型场景:同账号多端登录互踢导致 WS 每秒被后端关闭
|
||||
*/
|
||||
_recordCloseForFlapping() {
|
||||
const now = Date.now()
|
||||
this.recentCloseAt.push(now)
|
||||
// 丢掉窗口外的历史记录
|
||||
this.recentCloseAt = this.recentCloseAt.filter((t) => now - t <= FLAPPING_WINDOW_MS)
|
||||
if (this.recentCloseAt.length < FLAPPING_THRESHOLD) return
|
||||
if (now - this.lastFlappingNotifyAt < FLAPPING_NOTIFY_COOLDOWN_MS) return
|
||||
this.lastFlappingNotifyAt = now
|
||||
const count = this.recentCloseAt.length
|
||||
console.warn(`[WS] 检测到异常重连抖动:${FLAPPING_WINDOW_MS / 1000}s 内断开 ${count} 次,可能同账号在其他 tab/设备登录`)
|
||||
this._emit('_flapping', { count, windowMs: FLAPPING_WINDOW_MS })
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接断开时统一拒绝所有未完成的 ACK 等待(Task 9)
|
||||
* 避免业务层 await sendWithAck() 永久挂起
|
||||
|
||||
Reference in New Issue
Block a user