feat(m165): 下载引擎按数据根跨请求复用(运行时常驻 + 构造次数守卫)
宿主侧以前每次请求都现建一个 DownloadEngine:连接池、信号量限流、 健康度记忆、内容寻址缓存索引全部清零。现在改成按数据根共享一个实例。 - core: 新增 download::shared(进程级登记处,键 = 缓存根)—— engine_for / forget / constructions;region/health/cache/mirrors 的装配 收在这一个 build() 里(持锁构造只做本地 I/O,不联网) - core: download::blocking 改成进程内唯一常驻运行时。reqwest 的连接池绑在 驱动连接的运行时上,引擎复用必须配运行时常驻,否则复用的是个装满死连接的池 - presenter: host::engine::engine 返回 Arc<DownloadEngine>(共享实例), addon/update/versions/java/authlib 等全部调用点随之改;新增 forget 供 「重新检测地区」失效(否则进程级引擎下那句话不成立) - presenter: addon 详情页把「项目 + 版本列表」两次 block_on 收成一次 失败分类 / 镜像回退 / Retry-After 语义未改动(error.rs、http.rs、engine.rs 的 候选循环一字未动)。 守卫(都带变异实证): - shared::tests::one_engine_per_cache_root + host::engine::tests:: host_requests_share_one_engine:断言 constructions(root) == 1(不是只看结果) - shared::tests::pooled_connection_survives_across_block_on_calls:127.0.0.1 回环服务统计 accept 次数,两次 block_on 必须只拨号一次 验证:cargo test -p unlucky-core --lib(608 过)、5 个 download 集成 target(42 过)、 cargo test -p unlucky-presenter --lib(267 过)、cargo clippy --all-targets、 cargo fmt --check 全绿。真站点(Modrinth/CurseForge/BMCLAPI)未打,见报告。
This commit is contained in:
1 parent
b4e6fbf8c1
commit
46389d61fd
11 files changed
+486
-48
No files matched your search
@@ -4,35 +4,70 @@
|
||||
//! 但 headless 消费者(`crates/cli` 的 `ull`)不依赖 tokio,也不该为一次安装
|
||||
//! 把整个异步运行时依赖树拉进 CLI 的 manifest(D18:CLI 依赖树保持干净)。
|
||||
//!
|
||||
//! 因此把「建一个短命运行时 + 跑完整段 future」这两件事收在 core 里:
|
||||
//! 因此把「跑完整段 future」这件事收在 core 里:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let outcome = unlucky_core::download::blocking::block_on(async { /* 规划 + 下载 */ });
|
||||
//! ```
|
||||
//!
|
||||
//! 约定:**整段**安装流程包在一次 `block_on` 里(同一个运行时),不要每次下载都建一个——
|
||||
//! reqwest 的连接池绑定在创建它的运行时上,跨运行时会退化甚至报「dispatch task is gone」。
|
||||
//! # 一句话约定(M165 起)
|
||||
//!
|
||||
//! 这是给「单线程 CLI」用的逃生口,不是通用设施:app 层请直接用原生异步。
|
||||
//! **整个进程只有一个运行时**,所有 [`block_on`] 都跑在它上面。理由两条,都是实测口径:
|
||||
//!
|
||||
//! 1. **连接池绑在运行时上**。`reqwest` 的连接池条目由驱动连接的运行时任务持有:运行时一销毁,
|
||||
//! 池里的空闲连接就全成了尸体。M165 之前每次 `block_on` 现建一个临时运行时,于是下载引擎
|
||||
//! 的连接池**跨请求根本留不住**(每次重新 TCP + TLS);引擎复用([`super::shared`])与
|
||||
//! 运行时常驻必须成对出现,只做一半会退化成「复用一个装着死连接的池」。
|
||||
//! 2. **建运行时就是起线程**。每个请求建一次 = 每次多起两个线程,再各自销毁。
|
||||
//!
|
||||
//! 调用形状不变:把**一整段**流程包在一次 `block_on` 里仍然是对的(同一个 future 里少切几次
|
||||
//! 上下文,也少几次许可申请/释放);只是这几次调用现在落在同一个运行时上,不再互相孤立。
|
||||
//!
|
||||
//! # 仍然不能嵌套
|
||||
//!
|
||||
//! 在已有异步上下文里调 [`block_on`] 会 panic(tokio 的硬规则)。这条与 M165 之前一致。
|
||||
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// 在一个临时 tokio 多线程运行时上跑完 `future`。
|
||||
/// 共享运行时的 worker 数。
|
||||
///
|
||||
/// 运行时随调用结束销毁,因此适合「一条命令一次阻塞」的 CLI 生命周期。
|
||||
/// 下载流程里有真实的同步 I/O(`PartFile` 落盘、`write_atomic` 写索引/健康度),它们跑在
|
||||
/// worker 线程上;M165 之前每个请求自带 2 个 worker,现在这些请求共用同一池,所以按机器并行度
|
||||
/// 给一点余量(下限 2 保证单核机器也够驱动,上限 8 免得为一个启动器起一船线程)。
|
||||
fn workers() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map_or(2, std::num::NonZeroUsize::get)
|
||||
.clamp(2, 8)
|
||||
}
|
||||
|
||||
/// 进程内唯一的阻塞桥运行时(第一次 [`block_on`] 时创建,之后一直活着)。
|
||||
///
|
||||
/// 故意不实现关闭:它的生命周期就是进程的生命周期。静态量不会被 drop,因此进程退出时
|
||||
/// 也不会去等它「优雅停机」(那正是短命运行时被销毁时的开销)。
|
||||
fn bridge() -> &'static tokio::runtime::Runtime {
|
||||
static BRIDGE: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
||||
BRIDGE.get_or_init(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(workers())
|
||||
.thread_name("ull-blocking")
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("创建 tokio 运行时失败(内存/线程资源不足)")
|
||||
})
|
||||
}
|
||||
|
||||
/// 在进程共享的多线程运行时上跑完 `future`(阻塞当前线程)。
|
||||
///
|
||||
/// 多个线程可以同时调用:它们在同一个运行时上各自阻塞自己的线程,任务在同一个 worker 池里跑,
|
||||
/// 于是连接池、限流、健康度这些「跟运行时/引擎绑在一起」的东西真的能跨调用复用。
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// 运行时建不起来(系统资源耗尽)时 panic:此时进程已无法继续,早点暴露比静默降级好。
|
||||
/// 在已有 tokio 运行时上下文里调用同样会 panic(tokio 不允许嵌套 `block_on`)。
|
||||
pub fn block_on<F: Future>(future: F) -> F::Output {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.thread_name("ull-blocking")
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("创建 tokio 运行时失败(内存/线程资源不足)");
|
||||
runtime.block_on(future)
|
||||
bridge().block_on(future)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -52,4 +87,25 @@ mod tests {
|
||||
});
|
||||
assert!(start.elapsed() >= std::time::Duration::from_millis(5));
|
||||
}
|
||||
|
||||
/// 共享运行时能被多个线程同时驱动(宿主侧就是多线程并行发请求)。
|
||||
#[test]
|
||||
fn shared_runtime_serves_concurrent_callers() {
|
||||
let threads: Vec<_> = (0..4)
|
||||
.map(|index| {
|
||||
std::thread::spawn(move || {
|
||||
block_on(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||
index
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut seen: Vec<u32> = threads
|
||||
.into_iter()
|
||||
.map(|handle| handle.join().expect("线程不该 panic"))
|
||||
.collect();
|
||||
seen.sort_unstable();
|
||||
assert_eq!(seen, vec![0, 1, 2, 3]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user