chore: archive diagnostics and project documentation
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
# WebSocket 断线后使用新鉴权上下文重连:问题原因与修复方案
|
||||
|
||||
## 1. 对应提交
|
||||
|
||||
```text
|
||||
273550b fix(multimodal): reconnect websocket with fresh auth
|
||||
```
|
||||
|
||||
本文说明多模态 WebSocket 在运行过程中断开后,设备可能持续重连失败、无法继续唤醒对话,而重启设备后又可以恢复的问题。
|
||||
|
||||
## 2. 用户可见现象
|
||||
|
||||
典型表现如下:
|
||||
|
||||
1. 设备启动后能够正常连接并完成若干轮对话;
|
||||
2. 运行过程中 WebSocket 出现 error 或 disconnected;
|
||||
3. 后续日志反复出现连接失败,设备无法重新进入可用的语音会话;
|
||||
4. Wi-Fi 本身可能仍显示已连接;
|
||||
5. 重启设备后,连接通常能够立即恢复。
|
||||
|
||||
“重启后恢复”说明问题不一定来自远端服务长期不可用。如果远端服务或公网一直不可达,重启设备一般也不会立刻解决。这个现象更符合设备内部旧 WebSocket、旧鉴权 Header 或重连状态没有被完整清理。
|
||||
|
||||
## 3. 原有设计的问题
|
||||
|
||||
### 3.1 WebSocket 重连存在两个控制者
|
||||
|
||||
修复前,底层 WebSocket 客户端配置为:
|
||||
|
||||
```c
|
||||
.disable_auto_reconnect = false,
|
||||
.enable_close_reconnect = true,
|
||||
```
|
||||
|
||||
这意味着底层库可以自行重连。同时,项目的 control-plane 也负责鉴权、创建连接、处理断线和调度下一次启动。
|
||||
|
||||
因此一次断线后可能同时存在两条恢复路径:
|
||||
|
||||
```text
|
||||
底层 WebSocket 库
|
||||
└─ 使用原客户端和原 Header 自动重连
|
||||
|
||||
control-plane
|
||||
└─ 将会话标记为不可用,准备重新鉴权和创建连接
|
||||
```
|
||||
|
||||
两套状态机并不共享完整状态,容易产生以下不一致:
|
||||
|
||||
- control-plane 已认为连接不可用,但底层仍在自动重连;
|
||||
- control-plane 已清空当前会话鉴权信息,但旧客户端仍持有创建时的 Header;
|
||||
- 新一轮启动与旧客户端的关闭、回调或重连交错;
|
||||
- 上层看到“未连接”,底层却可能仍处于 connecting、closing 或 reconnecting;
|
||||
- 旧连接的异步回调可能影响新一轮连接。
|
||||
|
||||
### 3.2 WebSocket error/disconnected 没有强制回收旧 transport
|
||||
|
||||
修复前,`control_plane_multimodal_mark_transport_unavailable()` 会清理上层状态,但只有部分原因会触发底层 transport stop:
|
||||
|
||||
- 协议错误;
|
||||
- 文本发送失败;
|
||||
- listening 超时;
|
||||
- no-response 超时。
|
||||
|
||||
以下两个最直接代表 WebSocket 已失效的原因却没有进入强制关闭条件:
|
||||
|
||||
```text
|
||||
CONTROL_PLANE_TRANSPORT_LOSS_WS_ERROR
|
||||
CONTROL_PLANE_TRANSPORT_LOSS_WS_DISCONNECTED
|
||||
```
|
||||
|
||||
结果是上层状态已经退休,底层旧客户端却可能继续存在并自行重连。
|
||||
|
||||
### 3.3 断线事件没有稳定地调度新一轮连接
|
||||
|
||||
修复前,收到 transport error/disconnected 后主要执行:
|
||||
|
||||
- 清理待处理的云端业务状态;
|
||||
- 停止音频上行。
|
||||
|
||||
但没有在这两个事件中明确调度一次新的 WebSocket 启动。因此恢复可能依赖底层自动重连,或者依赖后续其他状态变化碰巧触发连接。
|
||||
|
||||
### 3.4 一次启动失败后,重试请求可能被消费掉
|
||||
|
||||
Wi-Fi 状态任务会消费 `ws_start_requested`。修复前,如果本次 `control_plane_try_start_multimodal_from_state_machine()` 返回失败,请求已经被消费,但没有立即重新挂起。
|
||||
|
||||
这样会出现:
|
||||
|
||||
```text
|
||||
收到重连请求
|
||||
↓
|
||||
本次启动失败
|
||||
↓
|
||||
请求被消费
|
||||
↓
|
||||
没有新的触发条件
|
||||
↓
|
||||
设备长期停留在未连接状态
|
||||
```
|
||||
|
||||
## 4. 根本原因
|
||||
|
||||
根本原因不是单次网络错误,而是 WebSocket 恢复流程的所有权不唯一:
|
||||
|
||||
```text
|
||||
底层库自动重连旧客户端
|
||||
+
|
||||
control-plane 清理并重建会话
|
||||
+
|
||||
WS 错误时旧 transport 未被强制回收
|
||||
+
|
||||
启动失败后重试请求可能丢失
|
||||
↓
|
||||
旧连接、旧 Header 和新会话状态互相干扰
|
||||
↓
|
||||
运行中持续连接失败,重启后因全部状态重建而恢复
|
||||
```
|
||||
|
||||
## 5. 修复方案
|
||||
|
||||
### 5.1 禁止底层 WebSocket 自动重连
|
||||
|
||||
配置改为:
|
||||
|
||||
```c
|
||||
.disable_auto_reconnect = true,
|
||||
.enable_close_reconnect = false,
|
||||
```
|
||||
|
||||
底层 transport 只负责一条连接的建立、收发和关闭,不再私自决定重连。
|
||||
|
||||
重连所有权统一交给 control-plane:
|
||||
|
||||
```text
|
||||
WebSocket 异常
|
||||
↓
|
||||
通知 control-plane
|
||||
↓
|
||||
退休旧 transport
|
||||
↓
|
||||
重新进入多模态启动流程
|
||||
↓
|
||||
重新取得当前有效鉴权信息并生成连接 Header
|
||||
↓
|
||||
创建新的 WebSocket 客户端
|
||||
```
|
||||
|
||||
### 5.2 WS error/disconnected 必须关闭旧 transport
|
||||
|
||||
以下原因被加入 `close_transport` 判断:
|
||||
|
||||
```text
|
||||
CONTROL_PLANE_TRANSPORT_LOSS_WS_ERROR
|
||||
CONTROL_PLANE_TRANSPORT_LOSS_WS_DISCONNECTED
|
||||
```
|
||||
|
||||
这保证上层判定 WebSocket 不可用时,底层旧客户端也会被停止,而不是留在后台继续使用旧状态重连。
|
||||
|
||||
### 5.3 断线和错误事件主动调度重连
|
||||
|
||||
处理以下事件时,明确调用 `control_plane_schedule_websocket_wait_task()`:
|
||||
|
||||
```text
|
||||
CONTROL_PLANE_MULTIMODAL_EVENT_TRANSPORT_DISCONNECTED
|
||||
CONTROL_PLANE_MULTIMODAL_EVENT_TRANSPORT_ERROR
|
||||
```
|
||||
|
||||
因此恢复不再依赖底层自动重连或其他偶发事件。
|
||||
|
||||
### 5.4 启动失败后重新挂起重试请求
|
||||
|
||||
Wi-Fi 状态任务尝试启动 WebSocket 后会检查结果:
|
||||
|
||||
```c
|
||||
if (!ws_started) {
|
||||
(void)control_plane_schedule_websocket_wait_task();
|
||||
}
|
||||
```
|
||||
|
||||
本次失败不会永久吃掉重试机会。后续仍由既有状态机和退避节奏继续尝试。
|
||||
|
||||
### 5.5 增加架构守卫
|
||||
|
||||
架构检查脚本增加规则,防止后续修改重新引入以下行为:
|
||||
|
||||
- 再次开启底层 WebSocket 自动重连;
|
||||
- WS error/disconnected 不再回收旧 transport;
|
||||
- transport error/disconnected 不再调度 control-plane 重连;
|
||||
- 启动失败后不重新安排重试。
|
||||
|
||||
这条守卫的目的不是运行时恢复,而是在构建或代码检查阶段阻止同类设计回退。
|
||||
|
||||
## 6. “fresh auth”的准确含义
|
||||
|
||||
这里的 `fresh auth` 更准确地说是:
|
||||
|
||||
> 不复用旧 WebSocket 客户端内部固化的鉴权 Header,而是销毁旧 transport,重新通过 control-plane 的鉴权与建连流程构造连接。
|
||||
|
||||
它不必然表示每次断线都强制向服务端申请一个全新的 JWT。设备鉴权模块仍可能复用尚未过期、权限范围正确的有效 JWT。
|
||||
|
||||
两者的区别是:
|
||||
|
||||
| 概念 | 含义 |
|
||||
| --- | --- |
|
||||
| 重新走鉴权建连流程 | 重新检查当前鉴权状态,并为新客户端生成当前有效的连接参数和 Header。此次修复保证做到这一点。 |
|
||||
| 强制刷新 JWT | 无论现有 JWT 是否有效,都向鉴权服务申请新 JWT。此次提交并没有要求每次都这样做。 |
|
||||
|
||||
## 7. 修复后的影响
|
||||
|
||||
### 正面影响
|
||||
|
||||
- WebSocket 重连只有一个控制者;
|
||||
- 每轮重连前会回收旧客户端;
|
||||
- 避免旧连接回调污染新会话;
|
||||
- 重连会重新经过项目自己的鉴权和状态检查;
|
||||
- 单次启动失败后仍能继续重试;
|
||||
- 运行中断线后的行为更接近重启设备后的干净启动流程。
|
||||
|
||||
### 代价
|
||||
|
||||
- 每次 WebSocket error/disconnected 都需要销毁并新建 transport;
|
||||
- 相比底层直接原地自动重连,会多经过一次清理和 control-plane 调度;
|
||||
- 如果需要刷新鉴权,还会增加鉴权请求时间;
|
||||
- 但换来的是状态一致性和可恢复性,避免设备陷入长期不可用状态。
|
||||
|
||||
该提交只重建多模态 WebSocket,不会主动让 Wi-Fi STA 断开并重新关联路由器。Wi-Fi 重关联属于另一层恢复策略。
|
||||
|
||||
## 8. 关键日志
|
||||
|
||||
修复后重点关注:
|
||||
|
||||
```text
|
||||
Schedule fresh-auth reconnect after transport disconnect
|
||||
Schedule fresh-auth reconnect after transport error
|
||||
Retire unavailable transport: reason=...; next start requires fresh auth
|
||||
```
|
||||
|
||||
预期顺序大致为:
|
||||
|
||||
```text
|
||||
transport error/disconnected
|
||||
↓
|
||||
Retire unavailable transport
|
||||
↓
|
||||
Schedule fresh-auth reconnect
|
||||
↓
|
||||
旧 transport 完成清理
|
||||
↓
|
||||
鉴权准备
|
||||
↓
|
||||
新 WebSocket 建立成功
|
||||
```
|
||||
|
||||
如果只看到反复的 schedule,但没有旧 transport 清理完成或新鉴权开始,应继续检查 transport stop 是否卡住。如果鉴权成功但新 WebSocket 仍连接失败,应继续区分 DNS、TCP/TLS 和远端拒绝问题。
|
||||
|
||||
## 9. 建议验证方法
|
||||
|
||||
1. 正常完成多轮语音对话,确认基础流程不受影响;
|
||||
2. 对话空闲时短暂关闭路由器或屏蔽网络,制造 WebSocket disconnect;
|
||||
3. 恢复网络,确认设备会退休旧 transport,并自动创建新连接;
|
||||
4. 在 TTS 播放或音频上行期间制造断网,确认上行停止且不会留下假连接;
|
||||
5. 连续重复断网、恢复至少 10 次,确认不需要重启设备;
|
||||
6. 检查每次新连接前是否都有明确的旧 transport 清理过程;
|
||||
7. 检查没有出现底层 WebSocket 自行无限重连的日志循环。
|
||||
|
||||
## 10. 涉及文件
|
||||
|
||||
```text
|
||||
components/control_plane/src/multimodal/control_plane_multimodal_runtime.c
|
||||
components/control_plane/src/multimodal/control_plane_multimodal_session_service.c
|
||||
components/control_plane/src/runtime/control_plane_runtime_workers.c
|
||||
components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c
|
||||
tools/check_architecture_guards.sh
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user