结论:常规 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 协商锁所有权,而不是在不确定写入者状态时全局清锁。
延伸阅读
- 上传队列的持久化与重试 —— 与本文常规 uploader 的另一条上传路径对照。
- 诊断日志转发与确认 —— 说明「回执」为什么不能等同于本地成功标记。
- URL 文件的范围缓存与完整性 —— 承接本文对上传字节数与对象摘要缺乏独立校验的讨论。
- 车联网队列车队遥测数据 —— 解释上传预算与网络费用在车队场景下的重要性。
参考资料
- 项目 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
发表回复