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:
1 parent
98262d3835
commit
e8544b19a7
15 files changed
+958
-200
No files matched your search
@@ -32,23 +32,23 @@
|
||||
//! 只 forget 的话它会一直用旧实例(含旧地区档案)直到进程结束。所以「重新检测地区」那条
|
||||
//! 设置动作除了 [`forget`] 还调 `CoreHost::refresh_update_factory()` 把新引擎接过去
|
||||
//! (M165 复审 P2-1:这是本模块文档原先说漏的一半)。
|
||||
//! * **并发多少**:`EngineConfig::default().max_concurrency`(64)现在是**进程级**上限,
|
||||
//! 不再是「每请求 64」。单请求独占时与从前一致;多请求并发时它们分摊这 64 张许可。
|
||||
//! * **并发多少**(M193):容量来自设置 `download.concurrency`(出厂 8、硬上限 64,见 [`set_tuning`]),
|
||||
//! 而且是**进程级**上限——多请求并发时分摊这批许可,不是「每请求 8」。
|
||||
//! * **镜像切换 / 失效**:一律走引擎原有的候选规划 + 健康度冷却,本模块不参与判定;唯一可见的
|
||||
//! 差别是健康度记忆现在真的跨请求累积(这本就是它存在的理由)。
|
||||
//!
|
||||
//! # 与 [`super::blocking::block_on`] 的绑定关系
|
||||
//!
|
||||
//! `reqwest` 的连接池绑在**驱动连接的运行时**上:引擎复用了而每次 `block_on` 都新起一个运行时,
|
||||
//! 池里的空闲连接就属于已经销毁的运行时,复用会退化成「拿到的都是死连接」。所以引擎复用必须
|
||||
//! 配运行时常驻(`blocking` 模块那头一并改成进程内唯一运行时)。两者是同一件事的两半。
|
||||
//! 池里的空闲连接就属于已销毁的运行时,复用会退化成「拿到的都是死连接」。所以引擎复用必须配
|
||||
//! 运行时常驻(`blocking` 那一头一并改成进程内唯一运行时)。两者是同一件事的两半。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use super::cache::{now_ms, ContentCache};
|
||||
use super::config::EngineConfig;
|
||||
use super::config::{publish, wanted, DownloadTuning, EngineConfig};
|
||||
use super::engine::DownloadEngine;
|
||||
use super::health::HealthRegistry;
|
||||
use super::mirrors::MirrorTable;
|
||||
@@ -102,9 +102,8 @@ pub fn engine_for(cache_root: impl AsRef<Path>) -> Arc<DownloadEngine> {
|
||||
/// 让该数据根的共享引擎失效(下次 [`engine_for`] 重新构造)。
|
||||
///
|
||||
/// **只让登记处不再持有那一份**:已经拿到 `Arc` 的人继续用旧的。短命请求自然结束;长命持有者
|
||||
/// (presenter 的自更新 `Factory`)必须自己重建,否则它会一直用旧实例——本仓在「重新检测地区」
|
||||
/// 的设置动作里调 `CoreHost::refresh_update_factory()` 补上这一步(见模块文档「`forget` 到什么范围」)。
|
||||
/// 返回是否真淘汰了一份。
|
||||
/// (presenter 的自更新 `Factory`)必须自己重建——「重新检测地区」那条设置动作就补调了
|
||||
/// `CoreHost::refresh_update_factory()`。返回是否真淘汰了一份。
|
||||
pub fn forget(cache_root: impl AsRef<Path>) -> bool {
|
||||
registry()
|
||||
.lock()
|
||||
@@ -116,9 +115,7 @@ pub fn forget(cache_root: impl AsRef<Path>) -> bool {
|
||||
/// 记一笔「这个缓存根上又造了一个引擎」。
|
||||
///
|
||||
/// **唯一调用点是 [`DownloadEngine::new`]**(不是 [`engine_for`]):记账卡在构造这个卡口上,
|
||||
/// 于是 [`constructions`] 统计的是**进程里真实发生过几次构造**,与「谁造的」无关。
|
||||
/// 若只在 `engine_for` 里记账,任何绕过登记处、自己 `DownloadEngine::new` 的调用点都统计不到
|
||||
/// (M165 复审备注 2 的诉求)。
|
||||
/// [`constructions`] 才是「进程里真实构造过几次」,与谁造的无关(M165 复审备注 2)。
|
||||
pub(crate) fn record_build(cache_root: &Path) {
|
||||
*ledger()
|
||||
.lock()
|
||||
@@ -127,10 +124,8 @@ pub(crate) fn record_build(cache_root: &Path) {
|
||||
.or_default() += 1;
|
||||
}
|
||||
|
||||
/// 该数据根在本进程里**实际构造过几次引擎**(不论谁构造的)。
|
||||
///
|
||||
/// 这是给守卫与诊断用的:跨请求复用成立时,同一个根无论被请求多少次,这个数都停在 `1`
|
||||
/// (`forget` 之后每重建一次 +1)。只断言「结果一样」测不出复用——引擎建几次都能下同一个文件。
|
||||
/// 该数据根在本进程里**实际构造过几次引擎**(不论谁构造的):复用成立时停在 `1`(`forget`
|
||||
/// 之后每重建一次 +1)。只断言「结果一样」测不出复用——引擎建几次都能下同一个文件。
|
||||
#[must_use]
|
||||
pub fn constructions(cache_root: impl AsRef<Path>) -> usize {
|
||||
ledger()
|
||||
@@ -141,9 +136,14 @@ pub fn constructions(cache_root: impl AsRef<Path>) -> usize {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// 真实构造(**唯一**一处 `DownloadEngine::new` 的宿主侧装配)。
|
||||
/// 真实构造(**唯一**一处 `DownloadEngine::new` 的宿主侧装配)。装配参数从该数据根的
|
||||
/// [`wanted`] 取,不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 →
|
||||
/// `attempts_for()` 上限、gh_proxy → 地区档案(`MirrorTable::expand()` 只认档案里那个前缀,所以
|
||||
/// 内容库 / 版本清单 / 字体 / 安装**所有**路径一起生效)。值由宿主翻译后经 [`set_tuning`] 灌入——
|
||||
/// core 不认识设置键名,也不依赖 `core::setting`。
|
||||
fn build(cache_root: &Path) -> DownloadEngine {
|
||||
let now = now_ms();
|
||||
let tune = wanted(cache_root);
|
||||
let region_path = cache_root.join("region.json");
|
||||
let region = load_cached(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
@@ -151,14 +151,30 @@ fn build(cache_root: &Path) -> DownloadEngine {
|
||||
profile
|
||||
});
|
||||
DownloadEngine::new(
|
||||
EngineConfig::default(),
|
||||
tune.apply_to(EngineConfig::default()),
|
||||
Arc::new(MirrorTable::builtin()),
|
||||
Arc::new(region),
|
||||
Arc::new(tune.apply_gh_proxy(region)),
|
||||
Arc::new(HealthRegistry::open_in(cache_root, now)),
|
||||
Arc::new(ContentCache::open(cache_root)),
|
||||
)
|
||||
}
|
||||
|
||||
/// 换该数据根的下载调优(宿主取引擎前 / 设置变更时调用),返回**是否真的变了**。变了就清掉该根的
|
||||
/// 登记项,下次 [`engine_for`] 按新参数重建——否则「改了设置要重启才生效」。已拿到 `Arc` 的长命
|
||||
/// 持有者继续用旧的(与 [`forget`] 同一条边界);自更新 `Factory` 由 dispatch 侧换引擎。
|
||||
/// **锁序**:`publish` 的锁先释放再拿登记处的锁,[`build`] 反之——两段不嵌套。
|
||||
pub fn set_tuning(cache_root: impl AsRef<Path>, next: DownloadTuning) -> bool {
|
||||
let key = key_of(cache_root.as_ref());
|
||||
if !publish(&key, next) {
|
||||
return false;
|
||||
}
|
||||
let _ = registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.remove(&key);
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -308,4 +324,76 @@ mod tests {
|
||||
assert_eq!(conns, 1, "两次下载应复用同一条连接");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// **守门①②**:设置说 2 ⇒ 引擎**真实的并发闸门**就是 2(探针读 `Semaphore`,不是读配置);
|
||||
/// 同一口装配点上的 gh-proxy 三档也真的改变 `plan()` 的候选链。
|
||||
///
|
||||
/// 变异证明(只改实现侧):① `build` 的 `tune.apply_to(...)` 换回裸 `EngineConfig::default()`
|
||||
/// ⇒ 探针 8,红;② `tune.apply_gh_proxy(region)` 换回 `region` ⇒ `off` 档仍有候选,红;
|
||||
/// ③ 删掉 `set_tuning` 里那条 `remove` ⇒ 换参数后交回旧引擎(探针 2 ≠ 5),红。
|
||||
/// gh-proxy 的**端到端**一半(schema 键 → 调优)要等 M197 登记该键,这里只守「调优 → 候选链」。
|
||||
#[test]
|
||||
fn the_setting_and_the_gh_proxy_override_reach_the_engine() {
|
||||
use super::super::types::{DownloadRequest, ResourceKind};
|
||||
use super::super::RegionProfile;
|
||||
|
||||
let dir = root("tune");
|
||||
// 镜像优先的档案:gh-proxy 只在这个模式下才生成候选。
|
||||
store_cached(
|
||||
&dir.join("region.json"),
|
||||
&RegionProfile::mirror_first(now_ms()),
|
||||
)
|
||||
.expect("地区档案");
|
||||
let req = DownloadRequest::new(
|
||||
ResourceKind::Other,
|
||||
"https://github.com/o/r/releases/download/v1/a.jar",
|
||||
dir.join("a.jar"),
|
||||
);
|
||||
let chain = |dir: &Path| -> Vec<String> {
|
||||
engine_for(dir)
|
||||
.plan(&req, 0)
|
||||
.into_iter()
|
||||
.map(|c| c.url)
|
||||
.collect()
|
||||
};
|
||||
let set = |c: Option<i64>, a: Option<i64>, g: &str| {
|
||||
set_tuning(&dir, DownloadTuning::from_settings(c, a, Some(g)))
|
||||
};
|
||||
|
||||
set(Some(2), Some(1), "off");
|
||||
let engine = engine_for(&dir);
|
||||
assert_eq!(engine.concurrency_probe(), 2, "闸门必须真是 2 张许可");
|
||||
assert_eq!(
|
||||
engine.config().max_attempts_per_source,
|
||||
1,
|
||||
"尝试次数同一条路"
|
||||
);
|
||||
let off = chain(&dir);
|
||||
assert!(!off.iter().any(|url| url.contains("gh-proxy")), "{off:?}");
|
||||
|
||||
set(Some(5), None, "https://gh.example/");
|
||||
let rebuilt = engine_for(&dir);
|
||||
assert!(!Arc::ptr_eq(&engine, &rebuilt), "参数变了必须换引擎");
|
||||
assert_eq!(rebuilt.concurrency_probe(), 5, "新闸门吃新值");
|
||||
let over = chain(&dir);
|
||||
assert!(
|
||||
over.iter()
|
||||
.any(|url| url.starts_with("https://gh.example/")),
|
||||
"覆盖前缀必须出现在候选链里:{over:?}"
|
||||
);
|
||||
|
||||
// 硬上限(纵深防御):宿主绕开 `from_settings` 直接塞 9999,闸门也只能开到 64。
|
||||
let wild = DownloadTuning {
|
||||
max_concurrency: 9_999,
|
||||
..DownloadTuning::default()
|
||||
};
|
||||
set_tuning(&dir, wild);
|
||||
assert_eq!(
|
||||
engine_for(&dir).concurrency_probe(),
|
||||
super::super::config::MAX_CONCURRENCY,
|
||||
"9999 张许可不能真开"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user