Files
lucky-cat/docs/knowledge vault/bugfix-downstream-stall-recovery.zh-CN.md
T

376 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 下行音频卡死后的会话恢复:问题原因与修复方案
## 1. 对应提交
```text
a94df95 fix(multimodal): recover stalled downstream sessions
```
本文说明云端 TTS 下行音频在播放过程中长时间不再推进时,设备可能一直停留在“云端正在响应”的状态,导致后续无法唤醒或开始下一轮对话的问题。
## 2. 用户可见现象
典型过程如下:
1. 设备已经成功唤醒并发送用户语音;
2. 云端开始返回 TTS,设备也开始播放;
3. 播放中途音频包停止到达,或播放器长时间没有进展;
4. 音频管线的保护逻辑终止本地播放;
5. 设备表面上不再播放,但云端会话仍可能保持 responding 或 active;
6. 后续再次说唤醒词或按键,无法顺利开始新会话;
7. 断开连接或重启设备后才恢复。
这个问题和“偶尔停顿后继续播放”不同。短暂网络抖动可以通过预缓冲或重缓冲吸收;这里处理的是已经超过保护阈值、被判定为不可继续等待的长时间 stall。
## 3. 原有逻辑的问题
### 3.1 stall 被当作普通 stopped
修复前,音频管线检测到下行播放长期无进展后,会推进 playback generation、停止播放任务并通知:
```text
MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED
```
但 `STOPPED` 本身可能代表多种正常情况:
- 用户主动打断;
- 上层主动停止;
- 会话正常切换;
- transport 关闭导致播放结束;
- 播放器因长时间无数据被保护逻辑强制终止。
把 stall 和正常 stopped 混在一起后,上层无法判断是否需要恢复云端会话。
### 3.2 本地播放器停止不等于云端任务结束
播放器只负责消费并播放已经收到的 Opus 数据。播放器停止后,服务端并不会自动知道设备已经放弃本次响应。
因此可能形成状态分裂:
```text
设备音频管线:已经停止播放
control-plane:仍认为任务存在
云端:仍认为本轮响应或任务未结束
WebSocket:物理连接可能仍显示 connected
```
此时“WebSocket 还连着”并不代表会话可复用。旧任务没有收尾,新一轮 RequestToSpeak、Start 或唤醒流程可能被旧状态阻塞。
### 3.3 直接重连会造成不必要的恢复开销
一旦 stall 就立刻关闭 WebSocket 虽然能清空状态,但会带来:
- 重新鉴权和建连延迟;
- 额外网络请求;
- 正常可用的 WebSocket 被浪费;
- 偶发的服务端响应异常被放大为完整断线。
因此需要先尝试只结束当前任务,失败后再退休连接。
## 4. 根本原因
问题的根本原因是下行播放状态和云端会话状态之间缺少针对 stall 的闭环:
```text
下行长时间没有新数据或播放进展
↓
音频保护逻辑停止本地播放器
↓
仅上报普通 STOPPED
↓
上层无法识别这是异常卡死
↓
没有向云端结束当前任务,也没有退休 transport
↓
旧会话长期保持 active,后续唤醒被阻塞
```
## 5. 修复方案
修复采用“两级恢复”,优先保留 WebSocket,无法收敛时再重连。
### 5.1 增加独立的 STALLED 事件
音频管线在保护 watchdog 判定响应卡死时,不再发送普通 `STOPPED`,而是发送:
```text
MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED
```
该事件逐层传递:
```text
audio pipeline
↓
platform callback
↓
domain event
↓
control-plane session service
↓
CONTROL_PLANE_MULTIMODAL_EVENT_DOWNSTREAM_STALLED
```
这样上层可以准确区分:
| 事件 | 含义 | 是否启动异常恢复 |
| --- | --- | --- |
| `STOPPED` | 主动停止、打断或普通停止 | 否 |
| `COMPLETED` | 正常播放完成 | 否 |
| `STALLED` | 下行响应长期无进展,被保护机制终止 | 是 |
### 5.2 只对云端响应启动会话恢复
session service 收到 `STALLED` 后,会检查当前是否确实存在云端响应:
- 当前有有效 task id;
- 并且云端仍处于 responding,或者本轮已经收到过云端音频。
如果只是本地提示音卡住,不会修改云端 WebSocket 状态,避免本地素材问题误伤云端连接。
### 5.3 第一级恢复:请求服务端 Stop
确认是云端下行响应 stall 后:
1. 停止音频上行;
2. 设置 `s_downstream_stall_stop_pending`;
3. 停止继续接收和播放本轮迟到音频;
4. 发送 `finish-task / Stop` 指令;
5. 最多等待 2000 ms 的服务端确认。
如果收到服务端 `Stopped`:
- 重置当前 session state;
- 停止残留的本地下行播放;
- 通知上层服务端已经不再 listening;
- 清除 stall recovery 状态;
- 保留当前 WebSocket,供下一轮对话复用。
这是成本最低的恢复路径。
### 5.4 第二级恢复:退休 transport 并重新连接
以下情况会进入第二级恢复:
- Stop 指令发送失败;
- 发送 Stop 后 2000 ms 内没有收到服务端确认;
- 当前连接已经断开。
对于前两种情况,旧 transport 会被标记为:
```text
CONTROL_PLANE_TRANSPORT_LOSS_DOWNSTREAM_STALL
```
随后执行:
```text
清除 stall recovery 状态
↓
将旧 transport 标记为不可用
↓
停止旧 WebSocket
↓
调度新的 WebSocket 启动
↓
重新进入鉴权和建连流程
```
这样即使服务端任务已经卡死、无法响应 Stop,设备也不会无限等待。
### 5.5 watchdog 负责有界等待
恢复流程由已有多模态 watchdog 推进,而不是在音频回调里阻塞等待。
内部记录三个状态:
```text
s_downstream_stall_recovery_pending
s_downstream_stall_stop_sent
s_downstream_stall_recovery_started_ms
```
它们分别表示:
- 是否存在待处理的 stall 恢复;
- Stop 是否已经发出;
- 等待 Stop 确认的起始时间。
这样音频线程只负责报告事实,控制层异步完成网络操作和超时处理,避免阻塞播放任务或 WebSocket 回调。
### 5.6 启停和连接异常时清理恢复状态
以下场景会主动清除 stall recovery 状态:
- transport disconnected;
- transport error;
- task failed;
- 多模态客户端重新启动;
- 多模态客户端停止。
目的是避免旧会话的 stall 标记污染下一条连接或下一轮任务。
## 6. 修复后的状态流转
### 6.1 服务端正常确认 Stop
```text
检测到下行 stall
↓
上报 STALLED
↓
确认是云端响应
↓
发送 Stop
↓
服务端返回 Stopped(不超过 2 秒)
↓
清理当前任务状态
↓
保留 WebSocket
↓
下一轮可以继续使用
```
### 6.2 Stop 失败或确认超时
```text
检测到下行 stall
↓
发送 Stop 失败,或等待超过 2 秒
↓
将旧 transport 标记为 downstream_stall
↓
关闭旧 WebSocket
↓
调度重新鉴权和建连
↓
恢复到可接受新会话的状态
```
### 6.3 本地提示音 stall
```text
检测到本地播放 stall
↓
没有有效云端响应
↓
仅结束本地播放
↓
不发送云端 Stop
↓
不关闭 WebSocket
```
## 7. 为什么等待时间是 2 秒
`CONTROL_PLANE_DOWNSTREAM_STALL_STOP_GRACE_MS` 当前为 2000 ms。
它不是播放提示音或采集音频的固定延迟,而只是 stall 已经发生后,等待服务端确认 Stop 的最长保护时间。
选择有界等待的原因是:
- 完全不等待会导致每次 stall 都立刻重连;
- 无限等待会让设备再次卡死;
- 2 秒给正常的服务端控制消息留出响应时间;
- 超时后仍能保证设备最终自愈。
在这 2 秒内,当前异常会话不会被当作正常会话继续使用。
## 8. 修复的影响范围
### 会改变的行为
- 云端 TTS 被确认 stall 后,会主动结束当前任务;
- Stop 无法确认时会关闭并重建 WebSocket;
- stall 恢复期间会停止音频上行并丢弃当前响应的迟到音频;
- 最坏情况下,下一轮对话需要等待旧连接退休和新连接建立。
### 不会改变的行为
- 正常 TTS 播放完成不会触发恢复;
- 用户主动打断产生的普通 stopped 不会误触发 stall 重连;
- 本地提示音 stall 不会主动关闭云端连接;
- 该提交不负责修复 Opus Decoder 内存泄漏;
- 该提交不负责 Wi-Fi STA 重新关联路由器;
- 该提交不解决短时抖动导致的普通卡顿,短时抖动由缓冲策略处理。
## 9. 关键日志
正常进入恢复时:
```text
Cloud downstream playback stalled; request bounded session recovery
Downstream stall recovery queued for watchdog worker
Downstream stall recovery started: request Stop and wait up to 2000 ms
Request Stop for stalled downstream response
```
服务端及时确认时:
```text
Downstream stall Stop acknowledged; transport remains reusable
Downstream stall recovered by server Stop acknowledgement; keep WebSocket
```
Stop 发送失败时:
```text
Downstream stall Stop request failed: ...; retire transport now
```
等待超时时:
```text
Downstream stall Stop acknowledgement timed out after 2000 ms; retire transport
```
如果进入 transport 退休,还应继续看到重新调度 WebSocket 的日志。
## 10. 建议验证方法
### 10.1 正常流程回归
1. 连续完成至少 20 轮正常对话;
2. 验证正常 TTS 完成不会出现 `DOWNSTREAM_STALLED`;
3. 验证用户按键打断和唤醒打断不会触发不必要的 WebSocket 重连。
### 10.2 stall 后服务端能够确认 Stop
1. 人为阻断下行音频数据,但保持 WebSocket 控制消息可用;
2. 等待本地 watchdog 判断 stall;
3. 确认设备发送 Stop;
4. 确认收到 `Stopped` 后保留原 WebSocket;
5. 立即开始下一轮唤醒,确认可以正常对话。
### 10.3 stall 后服务端不确认 Stop
1. 人为阻断服务端响应或所有 WebSocket 数据;
2. 确认设备等待不超过 2000 ms;
3. 确认旧 transport 被退休;
4. 恢复网络后确认设备自动建立新连接;
5. 确认无需重启设备即可再次唤醒。
### 10.4 本地提示音回归
1. 播放本地唤醒提示音或系统提示音;
2. 人为制造本地播放异常;
3. 确认日志显示本地 stall;
4. 确认没有发送云端 Stop,也没有关闭健康的 WebSocket。
## 11. 涉及文件
```text
components/control_plane/internal/control_plane_internal.h
components/control_plane/src/multimodal/control_plane_multimodal_runtime.c
components/control_plane/src/multimodal/control_plane_multimodal_session_service.c
components/domain/include/domain.h
components/domain/src/runtime/domain_runtime_adapters.c
components/platform/include/platform.h
components/platform/internal/multimodal/audio_pipeline.h
components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c
components/platform/src/runtime/platform_multimodal_runtime.c
```