Skip to content

fix(update): honor outer archive timeout - #4790

Merged
huangruiteng merged 6 commits into
loopx-project:mainfrom
Duang777:codex/core-bug-20260920
Sep 20, 2026
Merged

huangruiteng merged 6 commits into
loopx-project:mainfrom
Duang777:codex/core-bug-20260920

Conversation

@Duang777

Copy link
Copy Markdown
Contributor

Summary

  • propagate the remaining loopx update apply --timeout-seconds budget into the downloaded installer
  • let the commit archive curl use that budget instead of fixed 120/150 second limits
  • resume partial archive bytes across curl retries while preserving fail-closed extraction and activation

Closes #4735

Root cause

run_archive_installer bounded the downloaded Bash process, but scripts/install-from-github.sh independently capped the commit archive transfer at --max-time 120 --retry-max-time 150. A larger CLI timeout therefore could not help a slow but progressing archive download.

Validation

  • red/green regression: old code omitted LOOPX_INSTALLER_TIMEOUT_SECONDS and emitted archive curl limits 120/150; fixed code passes
  • 84 passed, 1 skipped across update, installer, runtime activation, ownership, and doctor tests
  • loopx-update-smoke ok
  • bash -n scripts/install-from-github.sh
  • Ruff passed on all changed Python files
  • control-plane TypeScript typecheck passed
  • loopx canary premerge --from-git-diff --tier standard: passed, 13/13 selected checks, no manual holds

The canary recorded one inherited maintainability advisory for existing module-size debt in loopx/chat_server.py and loopx/extensions/lark/goal_topic_connections.py; neither path is changed here.

Boundaries

  • changed surfaces: archive update download and installer transport only
  • no credentials, URLs, response bodies, raw logs, private state, or local paths are added to diagnostics
  • direct script invocation retains the existing 120/150 second defaults
  • future-facing pass: no adjacent refactor needed; the existing Python budget owner and Shell transfer owner remain the narrowest boundary

Propagate the remaining update budget into the archive installer and resume partial archive transfers across curl retries. This lets explicit longer update timeouts cover slow but progressing downloads while retaining fail-closed activation.

Fixes loopx-project#4735

Signed-off-by: duang777 <duangjl007@gmail.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审结论:REQUEST_CHANGES。审阅 head:534968d8a6d85c14c0cf835be5ead65bf51a656e

当前有 1 个 P1 阻塞:--continue-at - 与同一条 curl --retry 命令组合,并不会让 curl 在它自己的自动重试前重新读取目标文件大小。用一个支持 Range 的本地 HTTP 服务复现首轮超时后,服务端实际看到的两次请求都是 Range: null;第二次请求仍从字节 0 开始。最终校验和之所以正确,是因为第二次完整重下成功,而不是续传成功。现有测试只断言 --continue-at - 出现在 argv 中,无法发现这个语义差异。

最小修复建议:把归档传输改为受同一剩余 deadline 约束的显式多次 curl 调用,或采用另一种能够被实测证明在第二次请求发送 Range: bytes=<partial_size>- 的机制;保留当前 Python 外层硬超时、私有临时文件、完整归档 hash/解压/激活门禁。回归测试应让首轮在已写入确定字节数后超时,断言下一请求的 Range 起点、最终 SHA-256,以及终态失败时绝不解压部分文件。

动机

#4735 的问题是真实且边界清晰的:loopx update apply --timeout-seconds 600 的外层预算没有传入归档下载,内层仍固定使用 120/150 秒;慢速但持续有进度的链路会反复从头下载。这个 PR 选择现有的 run_archive_installerinstall-from-github.sh 两个 owner 来修复,而没有新增第二套下载器或持久状态,方向是对的。

实测确认其中一半目标已经实现:在相同的真实 HTTP + Bash 子进程夹具下,base 没有传入预算,安装脚本因缺少变量返回 1;当前 head 收到剩余的 6 秒并返回 0。问题是 PR 同时声称“跨 curl 重试续传”,而 real-path 证据否定了这部分,所以当前 head 还不能关闭 #4735

改动思路

调用链是 update apply --timeout-secondsexecute_update_planrun_archive_installer → 下载并执行 install-from-github.sh → archive curl → 既有 hash、解压和 install-local.sh

  • Python 的单调时钟 deadline 仍是硬预算权威;它在 bootstrap 成功后复制环境并注入 LOOPX_INSTALLER_TIMEOUT_SECONDS=floor(remaining),同时继续用 subprocess.run(..., timeout=remaining) 做最终硬停止。
  • Bash 在任何网络/临时文件副作用前校验该值为正整数;未提供时仍保留直接调用脚本的 120/150 默认值。
  • archive curl 使用这个值覆盖 --max-time--retry-max-time,但 --continue-at - 只在 curl 进程开始时根据已有输出文件决定 offset;同一进程里的自动 retry 没有重新计算 offset。因此这里需要“新的 resume-aware attempt”,而不只是追加一个 flag。

这个职责切分本身无需重构:Python 继续拥有总预算,Shell 继续拥有归档传输;只需修正 Shell 内部的 retry effect,并用真实 HTTP 行为而不是 argv mock 固化契约。

具体改动

生产代码共 15 行,测试 30 行;没有文档、生成物、状态 schema、权限或 CLI 新表面。

关键代码讲解

  1. loopx/self_update_download.py::run_archive_installer:bootstrap 成功后重新计算 deadline 余量,将其写入独立的 installer_env,并继续以同一余量约束 Bash 子进程。这个改动使外层预算真正抵达内层,同时不会污染调用者传入的环境对象。
  2. scripts/install-from-github.sh::installer_timeout_seconds 校验:空值代表兼容默认路径;非空值只能是正整数,非法输入在任何下载之前 exit 2。
  3. scripts/install-from-github.sh 的 archive curl:把传播值用于 --max-time/--retry-max-time 并增加 --continue-at -。预算覆盖有效,但单进程自动重试不具备作者假设的续传语义,这是本次阻塞点。
  4. tests/test_archive_installer_commit_response.py / test_installer_receives_remaining_outer_timeout_budget:前者捕获 curl argv,后者 mock subprocess.run 并检查环境变量;二者能证明 wiring,却都让 mock 直接提供成功后置条件,没有覆盖超时后的真实 Range 请求。

对主干的风险

正向与兼容路径表现良好:exact head 的 update/download/archive/runtime-activation 聚焦测试为 51 passed, 1 skipped;Ruff 和 bash -n 通过;相同改动 cherry-pick 到最新 main@04ba65ac1 后仍是 51 passed, 1 skipped。真实 base/head 子进程对比也证明预算从“缺失/返回 1”变为“收到剩余值/返回 0”。失败路径仍由 Python 外层 timeout 截断,部分归档不会进入 hash 后的解压与激活,fail-closed 性质未被破坏。

但最强反例仍失败:确定性 1 MiB payload 的首轮传输在 349525 字节后超时,curl 发起第二请求时仍没有 Range;这意味着慢速链路会继续浪费已下载字节,外层更大的预算只是让一次完整尝试更长,并没有实现累计进度。请用显式 resume-aware 重试修复,并让测试在旧实现/当前实现上真实失败、修复后通过。

语义与 CI 对齐

本 PR 复用了既有 --timeout-seconds 语义,没有创建新预算 vocabulary;直接脚本默认仍为 120/150。不过远端必需的 pytestmerge-gate 当前为失败:该分支比 main 落后 53 个提交,失败的两个 Lark 时间夹具和 maintainability ratchet 在最新 main 合成 checkout 中均已通过,说明它们不是这四个文件直接造成的回归,但仓库的 required-check receipt 仍必须通过 rebase 后的真实 CI 才能满足合并门禁,不能由本地合成结果代替。

我的整体评价

修复目标、owner 选择、改动体量和 fail-closed 边界都合理,预算传播本身也已被真实路径验证;不需要引入更大的更新框架。当前不能批准的原因不是风格或假想边缘情况,而是 PR 的核心可观察承诺之一在真实 curl 行为上被反证,且 required CI 仍是红色。

请完成 resume-aware retry + 真实 Range 回归、rebase 到当前 main,并让 required checks 转绿后再复审。除此之外,我没有发现权限、持久状态、诊断泄密或直接脚本默认值方面的新阻塞。

English verdict: REQUEST_CHANGES - head 534968d8a6d85c14c0cf835be5ead65bf51a656e; outer-budget propagation is valid, but a real timeout/retry probe shows both requests omit Range, so retries restart from byte zero, and required CI must be rerun after rebasing onto current main.

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
@Duang777

Copy link
Copy Markdown
Contributor Author

CI baseline note: the current run has exactly two failing test shards, and both failures reproduce on main@2caada69d after #4771:

  • test_a_lane_that_already_declares_a_priority_is_not_relabelled
  • test_real_canonical_provider_preserves_complete_complex_todo_semantics

Both stale integration assertions are isolated in #4819. The remaining shards and non-pytest checks on this head passed. I will merge current main into this branch and rerun the exact-head checks after the baseline repair lands; the curl resume fix itself remains unchanged.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

这个 PR 修复的是归档更新链路里一个真实的预算/重试语义缺口:loopx update apply --timeout-seconds 原先没有把剩余总预算传给归档安装器;上一版修复虽然加了 curl --retry--continue-at -,但同一个 curl 进程内部重试不会重新根据已落盘字节生成 Range 请求,因此网络已经取得的进度仍可能被丢弃。

我按当前 exact head 5a31b23085f1a3ab9c158b7b9605869c2029be16 重新审阅了完整调用链,也重点复核了上一次 review blocker 是否被真实 HTTP 行为而不只是参数字符串修掉。

改动思路

实现保留了既有职责边界:Python 侧继续拥有整次 update 的单调时钟 deadline,Shell 侧继续拥有归档传输与重试分类。关键变化是把剩余秒数注入单次安装器子进程,并把 curl 的内建重试改成最多三次、受同一 deadline 约束的独立 curl 调用。每次新调用都用 --continue-at - 重新读取临时文件大小,因此第二次请求能够发送精确的 Range: bytes=<partial_size>-

这比引入新的下载器、持久化缓存或第二套超时状态更小,也不会改变 checksum、解压和 activation 的既有所有权。

具体改动

关键代码讲解

  • loopx/self_update_download.py 在下载完 installer 后重新计算剩余预算,只把它放进该子进程的隔离环境,并仍用同一个剩余值作为 Python 进程超时。这样 Shell 内部的重试不会越过调用者的总 deadline。
  • scripts/install-from-github.sh 使用显式 attempt loop;只有 curl 传输错误或明确的瞬态 HTTP 状态才重试。成功完成归档之后才进入 SHA、解压和 install-local.sh,终态失败则由现有 EXIT trap 清理临时目录。
  • 新的 ThreadingHTTPServer 回归不是检查命令行形状,而是让首个响应只写入确定前缀、后续请求必须携带正确 Range 才返回 206。测试实际观察到请求序列 [无 Range, bytes=<partial_size>-],并核对最终 SHA。
  • 对应失败路径让续传请求持续返回 503,验证命令非零退出、tar 从未执行、临时目录被清理。因此 partial archive 不会被误解压或激活。

我在 exact head 上运行了两组聚焦测试:归档续传/timeout 组 33 passed,项目身份/activation 组 22 passed, 1 skipped(既有平台条件 skip),合计 55 passed, 1 skipped。同时 bash -n、变更 Python 的 Ruff 与 git diff --check 均通过。本轮按 capability 配置没有等待或引用远端 CI。

对主干的风险

风险集中在归档更新这一条路径。新实现仍依赖服务端支持标准 Range;不支持时 curl 会 fail closed,而不会静默从头覆盖或继续解压,这比接受不完整内容安全。直接执行脚本且未设置新环境变量时仍保留历史默认预算;没有新增 CLI、持久化状态、凭据回退或自动更新权限。

剩余风险主要是外部 GitHub/CDN 对 Range 的实际实现差异,但本地回归已经覆盖标准 200-partial → 206-resume 以及 repeated-503 的两条决定性语义路径。失败不会修改当前已安装 runtime,操作者可以安全重试。

我的整体评价

上一轮指出的核心问题已经在本次 exact head 被完整修复:续传现在由新的 curl 进程真实发出 Range,且与外层总预算、checksum、解压和 activation gate 保持一致。实现边界小、失败关闭、回归证据直接覆盖此前单看 argv 无法发现的缺口;没有发现新的 blocking finding。

未来向前看,本次没有必要再抽象通用 downloader:现有 Python deadline owner 与 Shell transport owner 已经足够清晰,进一步抽象会增加状态和维护面而不改善当前行为。

English verdict: APPROVE - head 5a31b23

@huangruiteng
huangruiteng merged commit e8e75a7 into loopx-project:main Sep 20, 2026
23 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Updater ignores outer timeout during commit archive download on slow links

2 participants