Files
Unlucky-Launcher/crates/core/src/setting/keys/download.rs
T
AskaEth 2ecb33afae 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 登记绑定。
2026-11-13 03:35:08 +08:00

119 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `download.*` 的七个键:四个基础键(M292 从 `keys.rs` 纯搬移拆出:那边 396 行、贴着 400 行硬线)
//! + M292 新增的三个超时/阈值键。
//!
//! 搬移范围与顺序都未改:并发 / 单源尝试次数 / 地区判定 / 镜像偏好,四项逐字搬来,主表只留
//! `.with_all(download::specs())` 一行。语义与「谁消费」仍写在各自的 `doc` 里(那是给界面与
//! AI 的 JSON Schema `description`,不是本文件的注释),所以这里不再抄第二份。
//!
//! 与 [`super::proxy`] 的分工:代理 / gh-proxy 三个键在那边(它们问的是「怎么把包拿到手」,
//! 而本文件这些问的是「怎么排队、怎么重试、从哪个源拿、允许慢到什么程度」)。
//!
//! **三个超时键的默认值与 M289 的硬编码逐值相等**(32 KiB/s / 120s / 60s),所以「不设」等于
//! 「M292 之前的行为」。**改它们的代价**只写在一处:`core::download::stall` 的模块头——本文件与
//! `download/config.rs` 都只指回那里,不各写一份。
//!
//! 三者的上界(`download::config` 的 `MAX_*` 常量)就是「把挂住变成无界」那道兜底:比它更大的值
//! 在写入口被 schema 拒、引擎侧再回落一次出厂值。
use crate::setting::spec::{AiPolicy, SettingSpec};
use super::{MIRROR_PREFERENCES, REGIONS};
/// 七个键的完整登记项(主表一行 `.with_all(download::specs())`)。
#[must_use]
pub fn specs() -> Vec<SettingSpec> {
vec![
SettingSpec::int(
"download.concurrency",
crate::download::config::DEFAULT_MAX_CONCURRENCY as i64,
(1, crate::download::config::MAX_CONCURRENCY as i64),
AiPolicy::silent(),
"同时下载的文件数(1~64,出厂 8)。这是引擎真实持有的并发许可,不是展示值;越界或手改坏值按出厂值生效",
),
SettingSpec::int(
"download.retry_limit",
crate::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE as i64,
(0, crate::download::config::MAX_ATTEMPTS_PER_SOURCE as i64),
AiPolicy::silent(),
"单个下载源的最多尝试次数(含首次;出厂 3)。0 与 1 都等于「只试一次、不重试」——0 是旧版 GUI 提供过的档位,保留合法但不代表「不下载」。换源不受此限",
),
SettingSpec::enumeration(
"download.region",
"auto",
REGIONS,
AiPolicy::audited(),
"下载地区判定:auto(时区+语言+探测)/ cn / global",
),
SettingSpec::enumeration(
"download.mirror_preference",
"auto",
MIRROR_PREFERENCES,
AiPolicy::audited(),
"镜像偏好:auto(健康度评分)/ official(官方源优先)/ mirror(镜像优先)",
),
// M292:三个超时/阈值键。默认值与 M289 的硬编码逐值相同(不设 = 当前行为),
// 「改它们要付什么代价」只写在 `core::download::stall` 的模块头,这里只给范围与默认值。
SettingSpec::int(
crate::download::config::KEY_THROUGHPUT_FLOOR,
i64::from(crate::download::config::DEFAULT_THROUGHPUT_FLOOR_KIB),
(
i64::from(crate::download::config::MIN_THROUGHPUT_FLOOR_KIB),
i64::from(crate::download::config::MAX_THROUGHPUT_FLOOR_KIB),
),
AiPolicy::silent(),
"分块下载的最低下限平均吞吐(KiB/s,4~4096,出厂 32):下载总期限 = 总字节 ÷ 这个数。调低能让慢链路下完,代价是真挂住的作业占用更久(见 core::download::stall 模块头)",
),
SettingSpec::int(
crate::download::config::KEY_CHUNK_STALL,
crate::download::config::DEFAULT_CHUNK_STALL.as_secs() as i64,
(
i64::from(crate::download::config::MIN_CHUNK_STALL_SECS),
i64::from(crate::download::config::MAX_CHUNK_STALL_SECS),
),
AiPolicy::silent(),
"分块下载「这么久没有任何新字节」就判失速(秒,30~600,出厂 120)。调大 = 卡住时报错更晚;与重试次数相乘后才是最坏等待",
),
SettingSpec::int(
crate::download::config::KEY_RESPONSE_TIMEOUT,
crate::download::config::DEFAULT_RESPONSE_TIMEOUT.as_secs() as i64,
(
i64::from(crate::download::config::MIN_RESPONSE_TIMEOUT_SECS),
i64::from(crate::download::config::MAX_RESPONSE_TIMEOUT_SECS),
),
AiPolicy::silent(),
"单次请求等响应头的上界(秒,10~600,出厂 60):只算到响应头到手,不含正文,所以对慢链路下大文件是安全的一档",
),
]
}
#[cfg(test)]
mod tests {
use super::*;
/// 七项的键名与顺序。搬移若换了顺序,`launcher_schema()` 的 `specs()` 投影顺序也跟着变
/// (那是可见行为:`ull config list` / AI 的 JSON Schema 都按它排),而 `keys.rs` 里那行
/// `.with_all(download::specs())` 看不出这件事。
///
/// 默认值 / 区间 / 与 `EngineConfig` 的同源关系不在这里守——那是
/// `download/config/tests.rs` 的 `the_timeout_keys_are_single_sourced_and_default_to_todays_values`
/// (跨 crate 模块的那份口径只写一处,别在这里抄第二份)。
///
/// 变异证明:把三个新键挪到 `region` 前面 ⇒ 红;把 `stall_timeout_secs` 改名 ⇒ 红。
#[test]
fn the_download_keys_are_registered_in_this_order() {
let keys: Vec<&str> = specs().iter().map(|spec| spec.key).collect();
assert_eq!(
keys,
[
"download.concurrency",
"download.retry_limit",
"download.region",
"download.mirror_preference",
"download.throughput_floor_kib",
"download.stall_timeout_secs",
"download.response_timeout_secs",
]
);
}
}