feat: add global RUST_ACCEL toggle to enable/disable rust acceleration at runtime

- Introduce RUST_ACCEL config to control all rust fast paths
- Fallback to Python implementations when disabled, preserving filter semantics
- Expose rust acceleration status in system info API
- Update CLI docs to reflect new toggle
- Add tests for runtime switch and fallback behavior
This commit is contained in:
jxxghp
2026-05-23 20:35:58 +08:00
parent 0bf228d29d
commit 0c133b7ccd
7 changed files with 151 additions and 17 deletions
+2
View File
@@ -42,6 +42,7 @@ from app.schemas import ConfigChangeEventData
from app.schemas.types import SystemConfigKey, EventType
from app.utils.crypto import HashUtils
from app.utils.http import RequestUtils, AsyncRequestUtils
from app.utils import rust_accel
from app.utils.security import SecurityUtils
from app.utils.url import UrlUtils
from version import APP_VERSION
@@ -514,6 +515,7 @@ async def get_env_setting(_: User = Depends(get_current_active_user_async)):
"AUTH_VERSION": SitesHelper().auth_version,
"INDEXER_VERSION": SitesHelper().indexer_version,
"FRONTEND_VERSION": SystemChain().get_frontend_version(),
"RUST_ACCEL_ENABLED": rust_accel.is_enabled(),
}
)
return schemas.Response(success=True, data=info)
+2
View File
@@ -480,6 +480,8 @@ class ConfigModel(BaseModel):
# ==================== 性能配置 ====================
# 大内存模式
BIG_MEMORY_MODE: bool = False
# Rust 加速总开关,关闭时所有 Rust 快路径回退到 Python 实现
RUST_ACCEL: bool = True
# 是否启用编码探测的性能模式
ENCODING_DETECTION_PERFORMANCE_MODE: bool = True
# 编码探测的最低置信度阈值
+32 -2
View File
@@ -4,7 +4,7 @@ from functools import lru_cache
from typing import List, Tuple, Union, Dict, Optional
from app.core.context import TorrentInfo, MediaInfo
from app.core.metainfo import MetaInfo, clear_rust_parse_options_cache, _rust_parse_options
from app.core.metainfo import lear_rust_parse_options_cache, _rust_parse_options
from app.helper.rule import RuleHelper
from app.log import logger
from app.modules import _ModuleBase
@@ -12,7 +12,6 @@ from app.modules.filter.RuleParser import RuleParser
from app.modules.filter.builtin_rules import BUILTIN_RULE_SET
from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey
from app.utils import rust_accel
from app.utils.string import StringUtils
_SIZE_UNIT = 1024 * 1024
@@ -155,6 +154,12 @@ class FilterModule(_ModuleBase):
mediainfo=mediainfo,
metainfo_options=_rust_parse_options() if self.__needs_metainfo_options(group_defs) else None,
)
if matched_orders is None:
return self.__filter_torrents_with_python(
groups=group_defs,
torrent_list=torrent_list,
mediainfo=mediainfo
)
ret_torrents = []
for index, pri_order in matched_orders:
torrent = torrent_list[index]
@@ -163,6 +168,31 @@ class FilterModule(_ModuleBase):
return ret_torrents
return torrent_list
def __filter_torrents_with_python(self, groups: List[dict],
torrent_list: List[TorrentInfo],
mediainfo: MediaInfo = None) -> List[TorrentInfo]:
"""
使用 Python 旧路径过滤种子,供 Rust 加速关闭或不可用时兜底。
"""
ret_torrents = torrent_list
parser = RuleParser()
parsed_rule_cache = {}
for group in groups:
rule_string = group.get("rule_string")
if not rule_string:
continue
ret_torrents = self.__filter_torrents(
rule_string=rule_string,
rule_name=group.get("name") or rule_string,
torrent_list=ret_torrents,
mediainfo=mediainfo,
parser=parser,
parsed_rule_cache=parsed_rule_cache,
)
if not ret_torrents:
break
return ret_torrents
def __needs_metainfo_options(self, groups: List[dict]) -> bool:
"""
判断当前规则链是否会触发 size_range,避免无大小规则时读取 MetaInfo 运行配置。
+37 -11
View File
@@ -1,5 +1,6 @@
from typing import List, Optional
from app.core.config import settings
from app.log import logger
try:
@@ -18,6 +19,31 @@ def is_available() -> bool:
return bool(_moviepilot_rust and _moviepilot_rust.is_available())
def is_config_enabled() -> bool:
"""
判断系统配置是否允许使用 Rust 加速。
"""
return bool(settings.RUST_ACCEL, True)
def is_enabled() -> bool:
"""
判断当前运行时是否实际启用 Rust 加速。
"""
return is_config_enabled() and is_available()
def status() -> dict:
"""
返回 Rust 加速能力与开关状态,供系统配置接口展示。
"""
return {
"available": is_available(),
"enabled": is_enabled(),
"import_error": str(_import_error) if _import_error else "",
}
def import_error() -> Optional[Exception]:
"""
返回 Rust 扩展导入失败的异常,便于调试构建问题。
@@ -29,7 +55,7 @@ def parse_filter_rule(expression: str) -> Optional[list]:
"""
使用 Rust 解析过滤规则表达式,不可用时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.parse_filter_rule_fast(expression)
@@ -45,12 +71,12 @@ def filter_torrents(
rule_set: dict,
mediainfo=None,
metainfo_options: Optional[dict] = None,
) -> list:
) -> Optional[list]:
"""
使用 Rust 执行完整种子过滤入口,返回原列表下标和优先级。
"""
if not _moviepilot_rust:
raise RuntimeError(f"Rust 扩展不可用,无法执行种子过滤: {_import_error}")
if not is_enabled():
return None
try:
return _moviepilot_rust.filter_torrents_fast(
groups,
@@ -61,8 +87,8 @@ def filter_torrents(
)
except BaseException as err:
_raise_non_rust_panic(err)
raise
logger.debug(f"Rust 种子过滤失败,回退 Python:{err}")
return None
def parse_indexer_torrents(
html_text: str,
@@ -75,7 +101,7 @@ def parse_indexer_torrents(
"""
使用 Rust 批量解析普通配置站点种子列表,不可用时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.parse_indexer_torrents_fast(
@@ -96,7 +122,7 @@ def parse_rss_items(xml_text: str, max_items: int = 1000) -> Optional[List[dict]
"""
使用 Rust 解析 RSS/Atom 条目,不可用或异常时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.parse_rss_items_fast(xml_text, max_items)
@@ -110,7 +136,7 @@ def parse_metainfo(title: str, subtitle: Optional[str] = None, options: Optional
"""
使用 Rust 从标题入口解析 MetaInfo,不可用或异常时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.parse_metainfo_fast(title, subtitle, options or {})
@@ -124,7 +150,7 @@ def parse_metainfo_path(path: str, options: Optional[dict] = None) -> Optional[d
"""
使用 Rust 从路径入口解析 MetaInfoPath,不可用或异常时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.parse_metainfo_path_fast(path, options or {})
@@ -138,7 +164,7 @@ def find_metainfo(title: str) -> Optional[dict]:
"""
使用 Rust 提取标题中的显式媒体标签,不可用或异常时返回 None。
"""
if not _moviepilot_rust:
if not is_enabled():
return None
try:
return _moviepilot_rust.find_metainfo_fast(title)
+1
View File
@@ -162,6 +162,7 @@ moviepilot install deps --config-dir /path/to/moviepilot-config
- 会在安装 Python 依赖后构建并安装 `moviepilot_rust` 加速扩展,因此本机需要可用的 Rust `cargo`
- 一键安装脚本会自动准备 Rust toolchain 和系统构建工具;手动执行 CLI 安装时,如果未安装 Rust 或本机编译器,请先安装后再执行 `moviepilot install deps`
- 如需临时跳过加速扩展构建,可设置 `MOVIEPILOT_SKIP_RUST_ACCEL=1`,但相关核心处理会回退到 Python 实现,性能收益不会生效
- 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作
安装前端 release
+45
View File
@@ -0,0 +1,45 @@
from app.core.config import settings
from app.utils import rust_accel
class _DummyRustExtension:
"""
测试用 Rust 扩展替身,用来验证总开关会阻止扩展调用。
"""
@staticmethod
def is_available() -> bool:
"""
模拟 Rust 扩展基础能力可用。
"""
return True
@staticmethod
def parse_filter_rule_fast(_expression: str) -> list:
"""
如果总开关关闭后仍调用扩展,测试应立即失败。
"""
raise AssertionError("Rust extension should not be called")
def test_rust_accel_runtime_switch_disables_fast_paths(monkeypatch):
"""
RUST_ACCEL 关闭时,即便扩展可用也应回退到 Python 路径。
"""
monkeypatch.setattr(settings, "RUST_ACCEL", False)
monkeypatch.setattr(rust_accel, "_moviepilot_rust", _DummyRustExtension())
assert rust_accel.is_available()
assert not rust_accel.is_enabled()
assert rust_accel.parse_filter_rule("HDR") is None
def test_rust_accel_status_reports_enabled_state(monkeypatch):
"""
状态接口应同时体现扩展可用性和配置开关后的实际启用状态。
"""
monkeypatch.setattr(settings, "RUST_ACCEL", True)
monkeypatch.setattr(rust_accel, "_moviepilot_rust", _DummyRustExtension())
assert rust_accel.status()["available"] is True
assert rust_accel.status()["enabled"] is True
+32 -4
View File
@@ -1,6 +1,7 @@
import unittest
from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import patch
from app.core.context import MediaInfo, TorrentInfo
from app.helper.torrent import TorrentHelper
@@ -128,13 +129,40 @@ class TorrentFilterTest(unittest.TestCase):
module._FilterModule__filter_torrents = fail_python_fallback
filtered = module.filter_torrents(
rule_groups=["test"],
torrent_list=[TorrentInfo(title="Movie", description="")],
)
with patch("app.modules.filter.rust_accel.is_enabled", return_value=True), \
patch("app.modules.filter.rust_accel.filter_torrents", return_value=[(0, 100)]):
filtered = module.filter_torrents(
rule_groups=["test"],
torrent_list=[TorrentInfo(title="Movie", description="")],
)
self.assertEqual(1, len(filtered))
def test_filter_torrents_uses_python_fallback_when_rust_disabled(self):
"""
Rust 加速关闭时应使用 Python 过滤路径,并保留规则优先级语义。
"""
module = _build_filter_module(
rule_string="HDR & !BLU > DV",
rule_set={
"HDR": {"include": "HDR"},
"DV": {"include": "DOVI"},
"BLU": {"include": "BluRay"},
},
)
torrents = [
TorrentInfo(title="Movie HDR WEB-DL", description=""),
TorrentInfo(title="Movie DOVI", description=""),
TorrentInfo(title="Movie HDR BluRay", description=""),
]
with patch("app.modules.filter.rust_accel.is_enabled", return_value=False):
filtered = module.filter_torrents(rule_groups=["test"], torrent_list=torrents)
self.assertEqual(torrents[:2], filtered)
self.assertEqual(100, filtered[0].pri_order)
self.assertEqual(99, filtered[1].pri_order)
def test_filter_torrent_keeps_extra_filter_semantics(self):
torrent = TorrentInfo(
title="Movie 1080p HDR",