feat(download): download.* 三个超时/阈值键接进 schema 与引擎(M292 2/2)

P21 第 1 条:M289 的四个超时/阈值只挂在 `EngineConfig`,`DownloadTuning::apply_to` 只搬并发与
每源尝试次数 ⇒ 慢链路用户没有任何自救手段。出厂等价于「平均吞吐 ≥32 KiB/s 才活得下来」:
188MB 在 20 KiB/s 上要 160min > 100.3min 预算,必被杀;被杀后不再重试、不再换源,所以是
「下不完」而不是「慢一点」。

## 键(schema 1.9.0,新键推 minor)

| 键 | 默认 | 范围 | 落到 |
|---|---|---|---|
| `download.throughput_floor_kib` | 32 KiB/s | 4~4096 | `EngineConfig::min_avg_throughput`(×1024 → 字节/秒)|
| `download.stall_timeout_secs` | 120s | 30~600 | `EngineConfig::chunk_stall_timeout` |
| `download.response_timeout_secs` | 60s | 10~600 | `EngineConfig::response_timeout` |

三个默认值与 M289 的硬编码**逐值相等** ⇒ 不设 = 当前行为。上界(`MAX_*` 常量)就是「把挂住变成
无界」那道兜底:越界值在写入口被 schema 拒、引擎侧再回落一次出厂值。

## 通路

`RawTuning`(宿主只按 `KEY_*` 常量取裸值)→ `DownloadTuning::from_raw`(默认/区间/回落/KiB→字节
全在 core 一处)→ `apply_to` → `EngineConfig` → `stall::job_budget(total, floor, min_throughput)`。
`job_budget` 内部 `min_throughput.max(1)` 挡除零(设置层已拒 0,这是纵深防御)。

两个宿主都读六个键:`crates/cli/src/download.rs::tuning`、`presenter::host::settings::download::
tuning_of`。CLI 在**非出厂值**时往 stderr 打一行 `DownloadTuning::summary()`(`--dry-run` 也会经过
它;实测 `ull --data-dir … java install 21 --dry-run` 的 stderr 是
「下载调优:并发=8 单源尝试=3 吞吐下限=16KiB/s 无进展窗口=300s 单次请求上界=180s gh_proxy=跟随内置默认」,
出厂/非法值时一行都不打)。

## 文档口径(各只写一处)

- `stall.rs` 模块头:「改这几个数要付的代价」(调低下限换到的是「慢」不是「无界」;两档窗口是
  单次尝试的界,最坏要乘候选数×尝试次数)+「数字从哪来」改成「默认值 / 能不能改」+ 补上 P21
  第 3 条要的「`permit_timeout` 是按一次尝试反推,不是一个作业」。

## 两个纯搬移(为 400 行硬线)

- `config.rs` 423 → 261:`DownloadTuning` / `RawTuning` / `secs_in_range` / `impl` 整段搬到
  `config/tuning.rs`(194 行),`pub use tuning::{DownloadTuning, RawTuning};` 保住原路径;
  搬移块 172 行原样 diff = 0(无归一化)。
- `stall.rs` 440 → 343:测试搬到 `stall/tests.rs`(103 行),唯一变换是整体去 4 格缩进;两个
  未改动的用例函数体与 HEAD 逐字符(去空白)相同。

## 用例

core(`config/tests.rs` + `stall/tests.rs` + `keys/download.rs`):默认值与 schema/引擎三处同源、
改设置 ⇒ 引擎配置与总期限都变、0 与越界回落且不除零、doc 里的数字、`summary()` 的生效值、键顺序。
cli:生产入口 `build_engine` 拿到三个新值 + `announce` 只在非出厂值时给行 + 非法值回落。
presenter:文件 → `tuning_of` → `EngineConfig` 三段一致 + 键名常量与 schema 登记绑定。
This commit is contained in:
AskaEth committed 2026-11-13 03:35:08 +08:00
1 parent f4b5a73594
commit 2ecb33afae
14 files changed
+950 -211

No files matched your search

+79
View File
@@ -264,3 +264,82 @@ fn missing_or_illegal_values_fall_back_to_the_factory_defaults_silently() {
);
}
}
/// **M292 / P21 判据的 CLI 侧闭合**:三个超时键从 `launcher.json` 进到**生产路径**的引擎配置,而且
/// 改过设置时终端上那行说明写的是**生效值**。
///
/// 为什么断言 [`super::announce`]:台账原文的判据是「改完能在 `--dry-run` 或日志里看到生效值」。
/// CLI 没有打印调优的 `--dry-run`(那几处 `--dry-run` 只出计划清单 / 版本串),所以这一行走
/// stderr,而它的内容由 `announce` 产出——**断言这个函数就是断言终端上那行**(不是另拼一份旁证)。
/// 走 stderr 而不是 `logs/launcher.log` 的理由见 `announce` 的文档:那个日志的目录是进程级全局,
/// 在装配点调 `set_dir` 会让一个读路径改全局状态(M292 第一版就是这么写的,`commands::sessions`
/// 的 launcher.log 用例当场红了)。
///
/// 抓得住:① `download::tuning` 少读一个键 ⇒ 引擎字段与那行说明同时红;② `announce` 对出厂值也返回
/// `Some` ⇒ 第二段红(每个 `ull install` 都多一行噪声);③ 把汇总写成常量 ⇒ 说明里的数字对不上。
/// 抓不住:真机慢链路上「预算到底够不够」(离线环境没有真链路,见 M292 报告的未验项)。
#[test]
fn the_timeout_settings_reach_the_engine_and_the_console_line() {
use std::time::Duration;
let dir = TempDir::new("timeouts");
let ctx = ctx_at(&dir);
write_launcher_config(
&ctx,
json!({ "download": {
"throughput_floor_kib": 16,
"stall_timeout_secs": 300,
"response_timeout_secs": 180,
}}),
);
// 出厂档不该打那一行(否则每个命令都多一行噪声)。
assert!(
super::announce(&DownloadTuning::default()).is_none(),
"出厂值不该打说明"
);
// 生产路径:`ull install` 取引擎的那个入口(四条命令共用这一处装配)。
let engine = crate::commands::install::build_engine(&ctx);
assert_eq!(
engine.config().min_avg_throughput,
16 * 1024,
"throughput_floor_kib=16 ⇒ 引擎里是 16 KiB/s(字节/秒)"
);
assert_eq!(
engine.config().chunk_stall_timeout,
Duration::from_secs(300)
);
assert_eq!(engine.config().response_timeout, Duration::from_secs(180));
// 终端上那行(同一份 `summary()`,同一个 `announce`)。
let line = super::announce(&super::tuning(&ctx)).expect("改过设置 ⇒ 该有一行说明");
assert!(line.contains("吞吐下限=16KiB/s"), "{line}");
assert!(line.contains("无进展窗口=300s"), "{line}");
assert!(line.contains("单次请求上界=180s"), "{line}");
// 不设 ⇒ 引擎回到出厂值,终端也不再打说明(「不设 = 当前行为」的 CLI 侧证据)。
write_launcher_config(&ctx, json!({ "download": {} }));
let engine = crate::commands::install::build_engine(&ctx);
assert_eq!(
engine.config().response_timeout,
Duration::from_secs(60),
"未设置 ⇒ 出厂 60s"
);
assert!(
super::announce(&super::tuning(&ctx)).is_none(),
"全出厂值不该打说明"
);
// 非法值:三个键一起写成 0 / 越界 ⇒ 回落出厂,且说明行不出现(不是「打一行假的」)。
write_launcher_config(
&ctx,
json!({ "download": {
"throughput_floor_kib": 0,
"stall_timeout_secs": 0,
"response_timeout_secs": 99_999,
}}),
);
assert_eq!(super::tuning(&ctx), DownloadTuning::default());
assert!(super::announce(&super::tuning(&ctx)).is_none());
}