Athena 主连接:70 秒 Ping 门不是定时器,跨会话队列不清空

作者:

在

结论:Athena 主 WebSocket 长连接的存活判断里有两处「看起来在保护、实际没有」。第一,70 秒 Ping 门不是独立定时器:只有当 recv_data() 抛 WebSocketTimeoutException 时才检查距上次 Ping 的年龄,业务帧持续到来时不会触发检查。第二,recv_queue、send_queue、log_recv_queue、upload_queue 都是模块级对象,新会话重建线程却不重建队列,旧请求或回复可能在断线后被新会话处理。此外 send_queue_push() 的队列未设 maxsize,4096 的单位是 Python 字符。以下为固定快照 c8fb906 的静态追踪,未连接生产服务或运行车载设备。

关键组件与调用链

设备到 Athena 的主连接由几个线程协作:一个接收线程、一个发送线程、一个 keepalive 线程、四个上传 worker、一个诊断日志 worker 以及 HANDLER_THREADS 个 RPC worker,全部由单个 end_event 管理。manage_athenad.main() 在子进程结束后等 5 秒再启动,这一层只观察进程退出,不验证 WebSocket 认证或 RPC 健康。

组件 行为与限制
manage_athenad.main() 子进程结束后等 5 秒再启动;不验证认证或 RPC 健康。
athenad.main() 读 DongleId 与上传队列;用 Api.get_token() 连 /ws/v2/{dongle_id},create_connection(timeout=30.0),成功后清当前上传字典。
handle_long_poll() 单个 end_event 管理收发/keepalive/四个上传/一个日志/RPC worker;exit_event 每 0.1 秒检查,逐个 join(),无统一 join 截止期。
ws_recv() 文本解码 UTF-8、二进制入 recv_queue;收到 Ping 写本机单调时钟纳秒到 LastAthenaPingTime。
ws_send() 从进程级优先队列取一条消息,按最多 4096 字符发首帧与 continuation;发送前已出队,异常只设结束事件不回填。
ws_manage() 依 IsOffroad 每 5 秒轮询切换 TCP keepalive/USER_TIMEOUT;无本地异常处理。
backoff() 返回 [0, min(128, 2**retries)) 的随机整数秒。

70 秒门不是定时器

Ping 写入的是 time.monotonic() 纳秒,不是 Unix 时间。ws_recv() 里的 70 秒门只在 recv_data() 抛 WebSocketTimeoutException 时检查:当底层持续返回业务帧却没有 Ping 时,循环不进入超时分支,源码不会检查 Ping 年龄;只有当底层持续超时、超过门限的那次超时才触发结束。这是控制流推导,实际帧与超时节奏取决于 WebSocket 与网络。

LastAthenaPingTime 只在收到 Ping 时更新,并在 main() 捕获连接异常或 manager 启动清理时删除;而收发 worker 设置 end_event 后正常退场的路径不删除它。因此某段时间内该 Params 可以保留旧 Ping 值,消费者必须用同一单调时钟域计算年龄,并区分「历史最后 Ping」与「当前会话活着」。hardwared.py 只是把它复制到 deviceState.lastAthenaPingTime,没有按年龄推导实时连接健康。

取值 含义 边界
70 秒 Ping 年龄门 仅在 recv 超时分支检查,不是独立定时器。
4096 字符 ws_send() 分帧长度 单位是 Python 字符,协议帧字节长度并非固定 4096。
5 秒 守护层重启间隔;ws_manage() 轮询间隔 仅观察进程退出,不代表 RPC 健康。
0.1 秒 exit_event 检查间隔 退出依赖各 worker 及时响应,无统一截止期。
128 backoff() 上限 首次异常可能等 0 或 1 秒。

状态转移与跨会话队列

连接成功后 main() 把 conn_retries 置 0 再调用 handle_long_poll()。收发线程异常会设置 end_event;所有 worker 结束、ws.close() 返回后,该次循环通常正常结束,随后执行 backoff(0) 即 0 秒后尝试下一次连接。只有异常穿透到 main() 的两个异常分支时才增加 conn_retries、随机退避并删除 LastAthenaPingTime。

跨会话的隐患在于队列:recv_queue、send_queue、log_recv_queue 和 upload_queue 是模块级对象,新会话重建线程却不重建这些队列,旧请求、回复或日志响应若在断线时留在队列,可能被新会话处理。cur_upload_items 在每次成功连接后清空,但上传等待队列不清空。这属于条件性时序风险,不是本轮复现结果。

send_queue_push() 对发送项使用 (priority, next(send_seq), data),同优先级由递增序号破同分,高优先级 RPC 回复可先于低优先级日志;队列未设置 maxsize,若生产速率长期超过发送速率,内存增长需要实测并设平台预算。此外 handle_long_poll() 逐个 join() 没有超时,上传回调、日志响应等待或阻塞的 WebSocket/Params 操作若不及时响应结束事件,会延迟整个会话清理和重连。

硬件与插件化验收

把主连接封装为带连接代号的会话插件:每条 RPC 请求、响应和诊断确认绑定代号、deadline 与幂等键;重连时显式取消、迁移或重试,不能让旧队列消息无条件进入新连接。把「进程存活、TCP 已连、服务端鉴权成功、最近 Ping 年龄、RPC 可用、上传可用」拆成独立健康指标,deviceState.lastAthenaPingTime 只能作为其中一项时间戳输入。

对队列长度、帧与消息字节数以及每类 worker 的执行时间设上限;所有 worker 应支持有限时间退出与可观测的清理结果;收发失败需区分未发送、部分发送、已发送未确认。台架验收应覆盖:仅业务帧无 Ping、静默链路、半开 TCP、快速反复断连、不同线程故障、Params 写失败、上传/日志等待阻塞、旧响应跨会话以及行车与驻车切换。现有硬件门控测试预期驻车约 60 秒、行车约 21 秒内触发重连并重新收到 Ping,但本轮未运行。

常见问题

Ping 超过 70 秒没来就一定会断开吗?

不一定。70 秒门不是独立定时器,只有在 recv_data() 抛 WebSocketTimeoutException 时才检查;当底层持续返回业务帧却没有 Ping 时,循环不进入超时分支,不会检查 Ping 年龄。

lastAthenaPingTime 能当作当前连接是否活着的判据吗?

不宜单独使用。它只在收到 Ping 时更新,且收发 worker 正常退场的路径不会删除旧值;消费者必须用同一单调时钟域算年龄,并区分「历史最后 Ping」与「当前会话活着」,该值只能算一项输入。

断线后旧队列里的消息会怎样?

可能被新会话处理。recv_queue、send_queue、log_recv_queue、upload_queue 是模块级对象,新会话重建线程但不重建这些队列;cur_upload_items 每次成功连接后清空,但上传等待队列不清空。

发送 4096 是固定 4096 字节吗?

不是。ws_send() 按最多 4096 字符拆分首帧与 continuation,单位是 Python 字符,协议帧的实际字节长度并非固定 4096;队列也未设 maxsize,长期积压需要实测。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/ATHENA_WEBSOCKET_SESSION_LIVENESS_DEEP_DIVE.md(取用了组件与调用链表、70 秒 Ping 门、跨会话队列不清空、backoff 与插件化验收要求)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注