feat(m193): 下载并发/重试/gh-proxy 真接进引擎(schema 与引擎同源常量)

- download.concurrency / download.retry_limit 以前引擎侧零消费者(恒用
  EngineConfig::default() 的 64,而 schema 说 8)。现在默认值与硬上限收成
  download/config.rs 的常量,schema、EngineConfig::default()、Semaphore 容量
  三处同源;宿主把三个键翻译成 DownloadTuning 后经 shared::set_tuning(按数据根)
  灌进装配点,改了设置下一次取引擎就生效。
- download.gh_proxy 三态(留空=跟随内置默认 / off=关 / 前缀=覆盖)下沉到
  shared::build,所有走共享引擎的下载路径(内容库/版本清单/字体/安装)一致。
- 删掉 MirrorTable::set_gh_proxy 与 default_gh_proxy 两个改了没用的入口。
- 下载设置页把三条假演示值换成说语义的中文行(含三态说明与 1~64 硬上限)。
- 守卫:探针读 Semaphore 真实许可数(不是读配置),每条配变异证明。
- docs/download-settings.md 记下取证表(另四个键仍零消费,已立 finding)。
This commit is contained in:
worker-dlmsettings authored and AskaEth committed 2026-11-09 05:24:11 +08:00
1 parent 98262d3835
commit e8544b19a7
15 files changed
+958 -200

No files matched your search

+21 -20
View File
@@ -5,18 +5,15 @@
//! ```text
//! 校验请求 → 内容寻址缓存直取(零网络)
//! → 候选规划(镜像表展开 + 健康度排序)
//! → Semaphore 限流
//! → Semaphore 限流 ← 容量来自设置(M193,见 config.rs)
//! → 候选源外循环(换源),每源内循环(指数退避 + 抖动)
//! → 续传判定(保守)→ 单连接 / 多连接分块 → 流式校验 → 原子落盘
//! → 失败则保留中间文件供下次续传
//! ```
//!
//! 最后一步保留的是**本会话命名的**中间文件(`<name>.ullpart-<session>-<seq>`,不再固定
//! 叫 `<name>.ullpart`):下次(甚至下次启动、重启后再下同一个文件)由 `PartFile::for_dest`
//! 在目标目录里认领它并改名成新的会话名,续传判定照旧。详细身份规则见 `atomic.rs` 模块头的
//! 「中间文件的身份」。
//!
//! 具体传输细节在 `transfer.rs`,错误分类在 `http.rs`,本文件只负责顺序与策略。
//! 中间文件是本会话命名的(`<name>.ullpart-<session>-<seq>`),下次(甚至下次启动)由
//! `PartFile::for_dest` 认领并改名,续传判定照旧(身份规则见 `atomic.rs` 模块头)。传输细节在
//! `transfer.rs`,错误分类在 `http.rs`,本文件只负责顺序与策略。
use std::path::Path;
use std::sync::Arc;
@@ -27,7 +24,7 @@ use tokio::sync::Semaphore;
use super::atomic::PartFile;
use super::backoff::Jitter;
use super::cache::{now_ms, ContentCache};
use super::config::EngineConfig;
use super::config::{EngineConfig, MAX_CONCURRENCY};
use super::error::{DownloadError, DownloadErrorKind};
use super::health::HealthRegistry;
use super::http;
@@ -72,8 +69,10 @@ impl DownloadEngine {
cache: Arc<ContentCache>,
) -> Self {
let http = http::build_client(&cfg).unwrap_or_else(|_| reqwest::Client::new());
let sem = Arc::new(Semaphore::new(cfg.max_concurrency.max(1)));
let seed = now_ms() ^ (u64::from(cfg.max_concurrency as u32) << 32);
// 硬上限夹取(M193):手改的 launcher.json / CLI 自拼的 cfg 都可能写着 9999,在唯一构造点再夹一次。
let permits = cfg.max_concurrency.clamp(1, MAX_CONCURRENCY);
let sem = Arc::new(Semaphore::new(permits));
let seed = now_ms() ^ (u64::from(u32::try_from(permits).unwrap_or(u32::MAX)) << 32);
// 纯记账(M165):卡在构造这个点上,才统计得到绕过登记处的调用点(守卫靠 `constructions`)。
super::shared::record_build(cache.root());
Self {
@@ -102,10 +101,15 @@ impl DownloadEngine {
&self.cfg
}
/// HTTP 客户端(同一条连接池与超时配置)。
///
/// 给「一锤子查询」用——例如 Adoptium 的版本查询(`core::game::java::api`):
/// 没必要为了一个 GET 再造一个 client,代理/超时/证书栈都在这里统一。
/// **真实并发闸门**此刻的许可数(探针,M193):读 [`Self::acquire`] 会去抢的那张
/// `Semaphore`,**不是** `config().max_concurrency`。有下载在飞时偏小,空闲等于容量。
#[must_use]
pub fn concurrency_probe(&self) -> usize {
self.sem.available_permits()
}
/// HTTP 客户端(同一条连接池与超时配置):给「一锤子查询」用(如 Adoptium 版本查询),
/// 没必要为了一个 GET 再造一个 client。
#[must_use]
pub fn client(&self) -> &reqwest::Client {
&self.http
@@ -243,7 +247,7 @@ impl DownloadEngine {
}
Err(err) => {
// 4xx(NotFound)不算「源坏」:院校镜像只留最新补丁,请求旧补丁必然 404,
// 记成失败会把唯一可用镜像冷却掉(M33/M34 实测:TUNA 每个目录只保 1 个归档)。
// 记成失败会把唯一可用镜像冷却掉(M33/M34 实测:TUNA 每目录只保 1 个归档)。
if err.kind == DownloadErrorKind::NotFound {
self.health.record_missing(
&candidate.source,
@@ -346,11 +350,8 @@ impl DownloadEngine {
})
}
/// 该源此刻给几次尝试:
///
/// * 冷却中 → 1 次(这次就是「恢复探测」,失败会重新冷却);
/// * 刚出冷却、还没洗白 → 也只给 1 次(`is_probe()`,别让死源每次到期都吃满 3 次);
/// * 正常 → 配置的 `max_attempts_per_source`。
/// 该源此刻给几次尝试:冷却中 / 刚出冷却未洗白 → 1 次(那就是恢复探测,别让死源每次到期
/// 都吃满预算);正常 → 设置里的 `download.retry_limit`(= `max_attempts_per_source`)。
pub(crate) fn attempts_for(&self, source: &str, kind: ResourceKind, now_ms: u64) -> u32 {
let health = self.health.health_of(source, kind);
if health.is_cooling_down(now_ms) || health.is_probe(now_ms) {