宿主侧以前每次请求都现建一个 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)未打,见报告。
112 lines
4.8 KiB
Rust
112 lines
4.8 KiB
Rust
//! 同步桥:让**非 async 宿主**驱动异步下载引擎。
|
||
//!
|
||
//! `core::download` 的引擎是异步的(tokio + reqwest),GUI 侧自带运行时;
|
||
//! 但 headless 消费者(`crates/cli` 的 `ull`)不依赖 tokio,也不该为一次安装
|
||
//! 把整个异步运行时依赖树拉进 CLI 的 manifest(D18:CLI 依赖树保持干净)。
|
||
//!
|
||
//! 因此把「跑完整段 future」这件事收在 core 里:
|
||
//!
|
||
//! ```ignore
|
||
//! let outcome = unlucky_core::download::blocking::block_on(async { /* 规划 + 下载 */ });
|
||
//! ```
|
||
//!
|
||
//! # 一句话约定(M165 起)
|
||
//!
|
||
//! **整个进程只有一个运行时**,所有 [`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;
|
||
|
||
/// 共享运行时的 worker 数。
|
||
///
|
||
/// 下载流程里有真实的同步 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 {
|
||
bridge().block_on(future)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::block_on;
|
||
|
||
#[test]
|
||
fn drives_a_plain_future_to_completion() {
|
||
assert_eq!(block_on(async { 1 + 1 }), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn drives_tokio_timers() {
|
||
let start = std::time::Instant::now();
|
||
block_on(async {
|
||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||
});
|
||
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]);
|
||
}
|
||
}
|