refactor: aria2 改手写原生异步 JSON-RPC,aria2p 与同步 HTTP 栈从依赖树清零

为什么:aria2p 内部是 requests + websocket-client 的同步实现,每个 RPC 都是一次
「同步网络 IO 藏在 to_thread 里」——线程池跑得再满也不产生真正的并发,每次轮询
还要额外付一次线程跳转。这是本项目最后一处此类残留,删掉后整个进程的网络 IO
都走同一个 aiohttp 连接池。

改动:
- music_downloader/_aria2.py 整体重写:新增 _Aria2Rpc,直接向 aria2 的
  JSON-RPC 2.0 端点发 getVersion / addUri / tellStatus / forceRemove /
  removeDownloadResult,一律走 core.http_client.request_once(不重试,与 aria2p
  口径一致);新增 Aria2RpcError 承载 HTTP 200 + error 对象的协议错误。
- _abort 逐条复刻 aria2p.remove(force=True, files=?, clean=True) 语义:终态走
  removeDownloadResult;在途先 forceRemove 再清结果表;控制文件 .aria2 无条件
  删除(clean=True 默认);仅当 files 且主移除成功才删数据文件。
- get_backend / _try_start 改原生异步:asyncio.Lock 单飞 + await rpc.get_version()
  探活,不再用 to_thread 包 subprocess.Popen;超时路径改 to_thread(_reap) 回收。
- 新增 Protocol _Rpc / _ProcessHandle(补 returncode 声明)——只声明本模块真正
  调用的方法,测试据此用轻量替身驱动状态机,不必依赖真实 aria2 进程。
- pyproject.toml 删 aria2p 依赖并同步更新过时注释;uv lock 连带移除 8 个传递
  依赖:requests / urllib3 / websocket-client / charset-normalizer / loguru /
  platformdirs / win32-setctime。至此全仓无任何 requests 栈。
- tests/test_aria2.py 重写为 21 项用例(_FakeRpc / _FakeProc / _LiveProc /
  _FakeHTTP 替身),新增终态体积守卫、_try_start 三分支(就绪重试 / 超时不就绪
  杀进程 / 进程提前退出)与 JSON-RPC 信封四例。
- tests/test_downloader.py 的 get_backend 替身由同步 lambda 改为协程——调用点
  已直接 await,同步替身会被 await 成 None 而报 TypeError。

验证(在 aria2p 已物理移除的环境上复核,故能真正证明无残留依赖):
- uv run ruff check .  → All checks passed!
- uv run pyright       → 0 errors, 0 warnings, 0 informations
- uv run pytest        → 392 项,0 失败 / 0 error / 0 skipped(基线 382,净增 10)
- data/site.db sha256 前后一致(a19f5dbf…6487),无 -wal/-shm 残留
This commit is contained in:
snowy-yang committed 2026-09-29 15:14:56 +08:00
1 parent 6c7f8f589e
commit 046225e38d
6 files changed
+516 -338

No files matched your search

+5 -3
View File
@@ -15,7 +15,6 @@ dependencies = [
"qrcode>=7.0,<8.3",
"python-multipart>=0.0.9",
"filetype>=1.2.0",
"aria2p>=0.12.1",
"mutagen>=1.48.1",
]
@@ -67,11 +66,14 @@ ignore = [
# 不是「用 sleep 当超时」的反模式。
"core/http_client.py" = ["ASYNC109"]
# ASYNC109:timeout 是**总下载时限**(交给 aria2 轮询循环判定),不是「用 sleep 当超时」。
"music_downloader/_aria2.py" = ["ASYNC109"]
# ASYNC240:搬迁后才出现的阻塞文件操作只有 unlink(删残片/控制文件)——目录项删除,
# 微秒级且不碰网络,卸载到线程的调度开销大于操作本身,与 downloader.py 同口径。
"music_downloader/_aria2.py" = ["ASYNC109", "ASYNC240"]
# ASYNC230/240:残留的阻塞文件系统调用都是**本地元数据/短写**——exists()/stat()/
# os.replace()/往 .part 顺序写分块,微秒级且不碰网络,卸载到线程的调度开销大于操作
# 本身。真正的重活(mutagen 全量重写标签、zipfile 整段拷贝、shutil.rmtree、同步
# SQLite 落库、aria2p 的同步 RPC)都已各自 to_thread 卸载。
# SQLite 落库)都已各自 to_thread 卸载;aria2 的 RPC 已改为手写原生异步实现
# (music_downloader/_aria2.py),不再是阻塞调用。
"music_downloader/downloader.py" = ["ASYNC230", "ASYNC240"]
"services/podcast.py" = ["ASYNC230", "ASYNC240"]
"services/batch.py" = ["ASYNC240"]