feat(download): 多线程下载 + 重试指数退避(issue #17)

下载主循环由串行改为「并发下载 + 串行簿记」三段式:
- Phase A:无网络前置校验(官方性/目标路径/建目录)+ 判定跳过/需下载,
  非官方 URL 在任何下载前 fail-fast
- Phase B:need-download 项经 scoped 线程池并发下载(默认并发 4,
  可配 1..=256)。worker 只做只读 &self 的 pull_one(各 URL 独立
  .part/目标文件),经 mpsc 把结果送回主线程;manifest/quarantine
  簿记与进度回调全在主线程串行执行,无需加锁。首个失败或 should_cancel
  置 cancel 标志,其余 worker 在任务边界停止
- Phase C:按 plan 顺序串行收尾——seed .hash 校验(顺序相关、可
  fail-fast)+ 构建有序结果

fail-fast 与「不发布不完整资源」不变量保留;进度事件按 URL 配对但
顺序不再单调(并发下天然如此)。

curl 重试加指数退避(网络类 200ms→400ms→800ms…上限 5s;ETXTBSY 仍走
极短退避),并发下对官方 CDN 更礼貌;退避基值 cfg(test) 下为 0 不拖慢
单测。

并发度经 OfficialUpdateConfig.download_concurrency 贯通,CLI
--download-concurrency 与 BAT_DOWNLOAD_CONCURRENCY 可配,.env 模板
与 USERGUIDE/CURRENT_STATUS/CHANGELOG 同步。

验证:新增并发正确性测试(并发 8:每 URL 恰一次 started+finished、
全部落盘)、并发度钳制、退避时长计算、CLI/env 解析单测;progress
排序测试改为顺序无关不变量;workspace 全测试(20 套件) + fmt +
clippy --all-targets -D warnings 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 03:25:04 -07:00
co-authored by Claude Fable 5
parent 8efd8f36b4
commit 0ab3f3b953
7 changed files with 449 additions and 78 deletions
+7 -2
View File
@@ -96,6 +96,8 @@ pub struct OfficialUpdateConfig {
pub audit_local: bool,
/// Repair local files when the local manifest audit fails.
pub repair: bool,
/// 下载并发度(并行执行的网络下载数)。默认 4,钳制到 `1..=256`。
pub download_concurrency: usize,
}
impl Default for OfficialUpdateConfig {
@@ -117,6 +119,7 @@ impl Default for OfficialUpdateConfig {
force: false,
audit_local: true,
repair: true,
download_concurrency: crate::official_download::DEFAULT_DOWNLOAD_CONCURRENCY,
}
}
}
@@ -898,7 +901,8 @@ impl OfficialUpdateService {
&active_resource_root,
&config.curl_command,
)
.with_proxy_config(config.curl_proxy.clone());
.with_proxy_config(config.curl_proxy.clone())
.with_download_concurrency(config.download_concurrency);
let snapshot_path = snapshot_path_for(config, &active_resource_root);
let bootstrap_cache_path = config.bootstrap_cache_path();
@@ -1337,7 +1341,8 @@ impl OfficialUpdateService {
&publish_plan.staging_path,
&config.curl_command,
)
.with_proxy_config(config.curl_proxy.clone());
.with_proxy_config(config.curl_proxy.clone())
.with_download_concurrency(config.download_concurrency);
report.staging_path = Some(publish_plan.staging_path.clone());
report.snapshot_path = staging_snapshot_path.clone();
report.download_manifest = staging_fetcher.download_manifest_path();