常规 uploader:候选选择、HTTP 结果与退避循环

作者:

在

结论:常规 uploader(system/loggerd/uploader.py)每轮只上传一个对象,且它的「成功」是本地终态,不是远端交付回执。源码用同一个 upload_success 事件与 user.upload=1 标记覆盖 200/201 与 401/403,把「零字节/超限文件跳过」也一并记为成功;因此不能据事件或 xattr 推断服务端已存完整对象。它也不是 Athena 的指定 URL 队列,两套逻辑要分开看。

函数级调用链

基线是本地 source/ 快照 c8fb906,结合静态阅读及四项原 upload() 假依赖探针;没有发网络请求、清锁、写真实扩展属性或运行设备测试。

函数 具体行为
get_directory_sort() / listdir_by_creation() 路段名按末个 -- 拆成两段、右对齐并在左侧补零后排序;这是目录名排序,不是读取文件创建时间。只选日志根下一层目录。
clear_locks() 进程启动时遍历根目录各子项,删除名称以 .lock 结尾的文件;未逐项确认对应写入者已退出。
list_upload_files() 跳过含任意 .lock 文件的目录及 user.upload=1 文件;计费网络下限制 qcamera.ts 的路段前缀。枚举不等于会被下游选中。
next_file_to_upload() 先物化全部候选到列表,第一轮按路径包含 crash/ 或 boot/ 选;第二轮只选 qlog、qlog.zst、qcamera.ts。没有候选则返回 None。
do_upload() / upload() 用设备令牌获取上传 URL/headers;按 key 与原文件扩展名决定是否压缩,然后 requests.put(..., timeout=10);压缩源完整进入内存缓冲。

排序与清锁两处都带假设。目录排序只是把路段名按末个 -- 拆开、右对齐补零后比较字符串,并非读取文件创建时间;清锁则在启动时直接删除所有以 .lock 结尾的文件,没有逐项确认对应写入者已退出。两者都只保证「本地看起来就绪」,不等于段已经写完或写入者已经停止。

选择与标记的精确语义

list_upload_files() 的产出是候选流,next_file_to_upload() 才决定本轮只上传一个对象。其第一优先级按完整路径中的 crash/、boot/ 子串匹配,第二优先级仅包含 immediate_priority 的三个文件名;普通路段 rlog、前后/座舱原始视频即使在候选流中,也不会被常规循环直接选中。还需注意 MAX_UPLOAD_SIZES 只含 qlog 和 qcam 文件名,不能把它当作对 qlog.zst 或 qcamera.ts 的通用大小上限。

传输实现还有一处容易被误解的地方:get_upload_stream() 对未压缩文件返回文件句柄,对压缩文件则先完整压缩到 BytesIO 再上传,所以「边上传边压缩」并不是本路径的实现;do_upload() 还忽略返回的压缩后长度,本层没有独立校验上传字节数与对象摘要。FAKEUPLOAD 会令非 412 的 URL 响应直接返回假成功,只适用于隔离测试,不是生产交付回执。

结果分支 源码处理 能/不能推断什么
零字节 / 命中上限且超限 不调用上传 API,但仍置 success=True 并写 user.upload=1。 只说明本地跳过,不说明远端收到。
412 URL API 或 PUT 的 412 记为 upload_ignored。 表示被忽略,不等于失败或成功。
200/201 与 401/403 都走名为 upload_success 的事件和本地标记。 不能据该事件或 xattr 推断服务端已存完整对象。
setxattr() 失败 通常只记事件,函数仍返回 True。 文件可能下轮被再次选择。

还有一个条件性错误路径:last_exc 只在尝试 do_upload() 的分支初始化;零字节或超限文件直接进入成功标记分支。如果此时 setxattr() 抛 OSError,其异常处理日志引用未定义的 last_exc,会引发 UnboundLocalError 并退出当前调用,外层 main() 没有包住 step()。假 xattr 探针分别复现了零字节与超限文件的原函数行为,但并未执行真实文件系统故障。另外,do_upload() 的 debug 日志还会记录预签名 URL 和 headers,这些值可能含短期访问凭据,生产日志策略需单独审计。

网络调度与测试边界

main() 从 deviceState 读 networkType/networkMetered,SubMaster.update(0) 后没有在这里核验消息新鲜度或外层有效位。FORCEWIFI 仅把用于「是否无网」的局部 network_type 设成 Wi-Fi,传给 step() 的仍是原始 deviceState 类型及计费位。

情形 睡眠/退避
无网 睡 60 秒(驻车)或 5 秒(行车)。
无候选 以同样基数加随机抖动等待。
成功后 基数 0.1 秒。
失败时 基数先翻倍、最大 120 秒,再睡基数到两倍之间。

UPLOADER_SLEEP=0 会跳过睡眠,适合测试而非目标硬件常态。test_uploader.py 用 mock URL API 与 FAKEUPLOAD 覆盖正常/412、目录顺序、锁文件和 xattr;它没有验证真实 HTTP PUT、401/403、预签名 URL 解析失败、零字节且 xattr 失败、计费网络状态切换或掉电恢复。测试中的 clear_locks 确认启动时会删除锁,但不证明活动 writer 不受影响。本轮未运行原仓库测试;四项隔离探针只覆盖 upload() 局部控制流。

自制硬件的上传插件契约

自制硬件的上传插件应把「未尝试/待传/传输中/被服务端接受/拒绝/本地跳过/确认存储」分别持久化,并以文件摘要或对象版本核对交付结果。上传器与 loggerd 应协商段关闭与锁所有权,不能在不确定写入者状态时全局清锁;对网络费用、RAM 中整文件压缩、带宽、eMMC 写放大和重试设预算。台架验收需要覆盖以上结果分支、网络状态失联、API/PUT 各类 HTTP 码、断电与 xattr 不可用,再验证实板资源峰值。

常见问题

uploader 报了 upload_success,文件一定上传成功了吗?

不一定。源码让 200/201 与 401/403 都走名为 upload_success 的事件和本地标记,零字节或超限文件也会置 success=True 并写 user.upload=1 但根本不调用上传 API。这些都是本地终态,不能据事件或 xattr 推断服务端已存完整对象。

为什么普通路段的 rlog 和视频不会被上传?

因为 next_file_to_upload() 第一轮只按路径包含 crash/ 或 boot/ 选,第二轮只选 qlog、qlog.zst、qcamera.ts 这三个 immediate_priority 文件名。普通路段的 rlog 与前后/座舱原始视频即使在候选流里,也不会被常规循环直接选中。

FORCEWIFI 会改变计费网络判断吗?

不会完全改变。源码中 FORCEWIFI 仅把用于「是否无网」的局部 network_type 设成 Wi-Fi,而传给 step() 的仍是原始 deviceState 类型及计费位。因此计费相关的限制仍按原始状态生效。

清锁会不会影响正在写入的日志?

clear_locks() 在进程启动时遍历根目录各子项,删除名称以 .lock 结尾的文件,但源码未逐项确认对应写入者已退出;测试里的 clear_locks 也只确认启动时会删除锁,不证明活动 writer 不受影响。因此自制硬件应让上传器与 loggerd 协商锁所有权,而不是在不确定写入者状态时全局清锁。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/UPLOADER_SELECTION_RESULT_BACKOFF_DEEP_DIVE.md(取用了候选选择与标记语义、结果分支、退避参数、条件性未定义变量路径与上传插件契约)。
  • 项目 02 —— openpilot-research/ATHENA_RECENT_ROUTES_METERED_UPLOAD_CONTRACT_DEEP_DIVE.md(仅用于确认计费网络最近路段门的接续关系,未取用其未公开内容)。

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

更新记录

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

评论

发表回复

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