Files
home-automation/docs/design/mqtt-reliability-fixes.md
T

125 lines
8.4 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.
# MQTT 可靠性修复 — 2026-10-11
本次范围为 MQTT-FIX-T01 → MQTT-FIX-T02,依赖顺序执行,两张卡自动继续。
按 [AGENTS.md](../../AGENTS.md) 与 [协作契约](README.md) 实现、独立审查及返工。
`done` 仅代表源码自动化技术验收完成;**源码修复完成后仍须人工部署与生产验证**。
中文实现、审查及返工简报存放于已忽略的 `review-notes/`,不入库。
## 证据与边界
排查日期 2026-10-11,Europe/Amsterdam。以下生产观察由用户提供,未在本轮访问生产确认:
1. 应用当天约 01:00 启动,故障前未自动重启、未发生 OOM。
2. 16:50:43 起出现 `MQTT disconnected unexpectedly (reason_code=Unspecified error).`。
3. 截至约 22:34 累计约 270 万条断线日志;22:34–22:35 一分钟内 15,283 次断线、101 次 publish rejected。
4. 一个 uvicorn 进程、284 个总线程;并非已经确认 284 个 MQTT 客户端。
5. 主 client ID 为 `home-automation`;配置最后更新时间为 2026-08-28,当天未观察到变更。
6. Modbus、WarmteLink 串口及独立 DSMR MQTT source 仍有新数据,不能描述为 broker 整体停止服务。
7. 生产镜像的 `app/main.py`、`app/integrations/mqtt.py` SHA256 与源码一致。
8. 用户在 broker 日志中看到大量新连接及 `session taken over`。
9. 22:37 用户授权重启应用后,观察超过一分钟未见新异常断线或发布失败,各采集源恢复;仅属临时恢复。
10. 首次断线触发原因未知,不能将网络故障或 broker 重启写成事实。
待独立核实的链路:60 秒 HA 发布任务在未连接时调用 `connect()`;它无条件创建同 ID Paho
client/线程、覆盖引用但未停旧 client;旧回调能覆盖共享连接状态。已有离线 fake 复现只是
排查线索,不能代替本轮回归测试。
部署背景:生产日志配置为 `{"Type":"json-file","Config":{}}`;约 276 万行日志,按内容
估算 JSON 文件约 353 MiB(非 stat 精确大小),当时约 666 GB 空闲,无即时容量危机。
生产 Compose 在 `/home/tianyu/.local/share/homeautomation`,与源码 Compose 是两个文件。
**本轮禁止**:push、tag、改写已推送历史、修改生产配置、重启/重建生产容器、连接真实
MQTT broker(包括 `10.238.75.82:1883`)、访问真实串口、改动生产数据库或日志;禁止
清空历史日志、删除 Docker 文件、prune、变更全局 Docker daemon。仅提供部署步骤。
允许源码/测试/部署文档修改、本地 commit、fixup 及整理本轮尚未推送提交。保留已有无关改动。
### MQTT-FIX-T01 — 修复主 MQTT 客户端生命周期
- **Status**: `done`
- **Depends**: none
- **Context**: 防止短暂断线后定时任务创建同 ID 客户端累积,避免旧回调污染新连接。
**Files**
- `modify app/integrations/mqtt.py`
- `modify app/main.py`
- `modify tests/test_mqtt_client.py`
- `modify tests/test_mqtt_subscribe.py`
- `create tests/test_mqtt_scheduler.py`(必要的调度任务回归)
- `modify README.md`(MQTT 生命周期说明)
- `modify .gitignore`(编排过程报告目录保护)
- `create docs/design/mqtt-reliability-fixes.md`(主线维护任务与 Status)
**Steps**
1. 独立核实当前生命周期、回调及调度代码,结合安装的 Paho 2.1.0 源码核对启动顺序和失败清理。
2. 使同配置 `connect()` 幂等,区分正在连接、等待自动重连、无客户端;现有 client 能自动重连时不再创建额外 client。
3. 初始连接失败、初始化失败、后台循环失效均须可恢复,失败对象/循环清理,不能永久跳过重试。
4. 配置变更安全停止、清理旧 client 后替换,受管理的主 client 只有一个。
5. connect/disconnect/message 回调核验身份或 generation,拒绝旧状态及过期消息。
6. 处理创建、替换、关闭与回调并发;不能持有回调需要的锁调用等待网络线程退出的 `loop_stop()`。
7. 保持独立 DSMR source、订阅恢复、MQTT 测试身份及现有 publish 行为兼容。
8. 增加离线故障与并发回归,更新生命周期说明,运行适用闸门并保存中文实现简报。
**Out of scope / 不要碰**
- 不使用随机 ID、单纯换 ID 或降低日志级别掩盖问题。
- 不改变 HTTP 路由/schema/docstring;如确有必要变更,须同步 OpenAPI 和前端类型。
- 不改变旧 milestone 完成状态;不访问生产或真实设备;不处理已有 Compose 串口改动。
**Acceptance criteria**
- [x] 同配置重复 connect 及断线后多轮定时任务不累积 client 或后台循环。
- [x] 初次连接/初始化失败后能重试,失败对象和循环被清理,循环失效也有恢复路径。
- [x] 配置重连后旧 client 停止,旧 connect/disconnect/message 回调不能影响新状态或处理过期消息。
- [x] 当前 client 重连成功后恢复订阅与发布。
- [x] shutdown、reconnect 与回调交错时能完成、不死锁。
- [x] 新测试使用 mock/fake 及合成配置,不连接生产 broker 或真实串口;覆盖行为与并发边界。
- [x] DSMR source、测试 client ID 及现有 publish 行为兼容。
- [x] pytest、ruff 全绿,独立 Reviewer 重跑;如适用同步契约/检查构建上下文。
**Reviewer checklist**
- 独立核查 Paho 2.1.0 生命周期顺序、线程泄漏、状态污染、死锁及失败恢复,不仅断言内部实现细节。
- 核查调度触发路径、多轮断线自动重连、配置替换与关闭交错,以及旧消息拒绝。
- 核查 DSMR 独立客户端及订阅/publish/test identity 回归风险。
- 先读对应实现/返工简报,独立重跑闸门;报告 PASS 或编号返工清单,不改实现或 Git。
### MQTT-FIX-T02 — 为 Docker 日志配置轮转
- **Status**: `todo`
- **Depends**: MQTT-FIX-T01
- **Context**: 为本项目容器限定日志保留,明确源码修改与生产容器应用配置是两个操作。
**Files**
- `modify docker-compose.yml`
- `modify docker-compose.dev.yml`(仅必要时)
- `modify README.md`(部署说明)
- `modify tests/test_deployment.py`(必要的静态检查)
- `modify docs/design/mqtt-reliability-fixes.md`(主线维护 Status)
**Steps**
1. 为 app 与 migration 均配置 `logging.driver: json-file`,字符串 options `max-size: "10m"`、`max-file: "3"`。
2. 验证开发 override 合并后保留相同合理的轮转配置,不修改全局 daemon 或其他项目。
3. 用 `docker compose config --quiet` 或等效静态检查验证基础与合并配置;不输出含凭据的完整配置。
4. 说明宿主机日志路径 `/var/lib/docker/containers/<完整ID>/<完整ID>-json.log`、`docker logs`、保留策略。
5. 明确仅 `docker restart` 不应用 logging 配置,需要重建容器。
6. 给出生产目录 `/home/tianyu/.local/share/homeautomation` 中的最小配置同步、重建、验证与回退步骤。
7. 写清发布源码修复镜像与仅应用日志配置各自前提,以及短暂采集中断;不覆盖生产设备/volume/env 等配置。
8. 保存中文简报,跑 pytest / ruff 和配置验证,独立 Reviewer 核查可执行性。
**Out of scope / 不要碰**
- 不自动部署,不清空或删除日志,不 prune,不触碰设备历史数据。
- 不修改全局 Docker daemon;不把源码配置变更描述为生产已生效。
- 不纳入用户已有的串口映射/group_add 改动。
**Acceptance criteria**
- [ ] app、migration 最终 Compose 均含指定字符串 logging options。
- [ ] 开发 override 合并后日志配置正确。
- [ ] Compose 经 config --quiet 或等效静态检查,不输出凭据。
- [ ] 文档说明日志路径、docker logs、保留策略、restart 无效而须重建。
- [ ] 文档明确生产路径、最小同步、镜像更新前提、采集中断、验证与回退;区分镜像发布和日志配置应用。
- [ ] 无生产操作、数据/日志删除,已有无关改动保留。
- [ ] pytest、ruff 全绿,独立 Reviewer 重跑并核查合并及部署可执行性。
**Reviewer checklist**
- 核查 logging options 类型及基础/开发配置合并;静态配置检查的范围和凭据保护。
- 核查生产/source Compose 分离,镜像构建发布前提与 logging 重建步骤是否明确。
- 核查只涉及 app/migration,不变更串口、volume、生产 DB、全局 daemon;说明旧日志在重建时的 Docker 生命周期影响及可回退方式。
- 先读对应实现/返工简报,独立重跑闸门,给 PASS 或编号返工清单。