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.schemas.types import SystemConfigKey, EventType
from app.utils.crypto import HashUtils from app.utils.crypto import HashUtils
from app.utils.http import RequestUtils, AsyncRequestUtils from app.utils.http import RequestUtils, AsyncRequestUtils
from app.utils import rust_accel
from app.utils.security import SecurityUtils from app.utils.security import SecurityUtils
from app.utils.url import UrlUtils from app.utils.url import UrlUtils
from version import APP_VERSION 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, "AUTH_VERSION": SitesHelper().auth_version,
"INDEXER_VERSION": SitesHelper().indexer_version, "INDEXER_VERSION": SitesHelper().indexer_version,
"FRONTEND_VERSION": SystemChain().get_frontend_version(), "FRONTEND_VERSION": SystemChain().get_frontend_version(),
"RUST_ACCEL_ENABLED": rust_accel.is_enabled(),
} }
) )
return schemas.Response(success=True, data=info) return schemas.Response(success=True, data=info)
+2
View File
@@ -480,6 +480,8 @@ class ConfigModel(BaseModel):
# ==================== 性能配置 ==================== # ==================== 性能配置 ====================
# 大内存模式 # 大内存模式
BIG_MEMORY_MODE: bool = False BIG_MEMORY_MODE: bool = False
# Rust 加速总开关,关闭时所有 Rust 快路径回退到 Python 实现
RUST_ACCEL: bool = True
# 是否启用编码探测的性能模式 # 是否启用编码探测的性能模式
ENCODING_DETECTION_PERFORMANCE_MODE: 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 typing import List, Tuple, Union, Dict, Optional
from app.core.context import TorrentInfo, MediaInfo 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.helper.rule import RuleHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase 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.modules.filter.builtin_rules import BUILTIN_RULE_SET
from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey
from app.utils import rust_accel from app.utils import rust_accel
from app.utils.string import StringUtils
_SIZE_UNIT = 1024 * 1024 _SIZE_UNIT = 1024 * 1024
@@ -155,6 +154,12 @@ class FilterModule(_ModuleBase):
mediainfo=mediainfo, mediainfo=mediainfo,
metainfo_options=_rust_parse_options() if self.__needs_metainfo_options(group_defs) else None, 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 = [] ret_torrents = []
for index, pri_order in matched_orders: for index, pri_order in matched_orders:
torrent = torrent_list[index] torrent = torrent_list[index]
@@ -163,6 +168,31 @@ class FilterModule(_ModuleBase):
return ret_torrents return ret_torrents
return torrent_list 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: def __needs_metainfo_options(self, groups: List[dict]) -> bool:
""" """
判断当前规则链是否会触发 size_range,避免无大小规则时读取 MetaInfo 运行配置。 判断当前规则链是否会触发 size_range,避免无大小规则时读取 MetaInfo 运行配置。
+37 -11
View File
@@ -1,5 +1,6 @@
from typing import List, Optional from typing import List, Optional
from app.core.config import settings
from app.log import logger from app.log import logger
try: try:
@@ -18,6 +19,31 @@ def is_available() -> bool:
return bool(_moviepilot_rust and _moviepilot_rust.is_available()) 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]: def import_error() -> Optional[Exception]:
""" """
返回 Rust 扩展导入失败的异常,便于调试构建问题。 返回 Rust 扩展导入失败的异常,便于调试构建问题。
@@ -29,7 +55,7 @@ def parse_filter_rule(expression: str) -> Optional[list]:
""" """
使用 Rust 解析过滤规则表达式,不可用时返回 None。 使用 Rust 解析过滤规则表达式,不可用时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.parse_filter_rule_fast(expression) return _moviepilot_rust.parse_filter_rule_fast(expression)
@@ -45,12 +71,12 @@ def filter_torrents(
rule_set: dict, rule_set: dict,
mediainfo=None, mediainfo=None,
metainfo_options: Optional[dict] = None, metainfo_options: Optional[dict] = None,
) -> list: ) -> Optional[list]:
""" """
使用 Rust 执行完整种子过滤入口,返回原列表下标和优先级。 使用 Rust 执行完整种子过滤入口,返回原列表下标和优先级。
""" """
if not _moviepilot_rust: if not is_enabled():
raise RuntimeError(f"Rust 扩展不可用,无法执行种子过滤: {_import_error}") return None
try: try:
return _moviepilot_rust.filter_torrents_fast( return _moviepilot_rust.filter_torrents_fast(
groups, groups,
@@ -61,8 +87,8 @@ def filter_torrents(
) )
except BaseException as err: except BaseException as err:
_raise_non_rust_panic(err) _raise_non_rust_panic(err)
raise logger.debug(f"Rust 种子过滤失败,回退 Python:{err}")
return None
def parse_indexer_torrents( def parse_indexer_torrents(
html_text: str, html_text: str,
@@ -75,7 +101,7 @@ def parse_indexer_torrents(
""" """
使用 Rust 批量解析普通配置站点种子列表,不可用时返回 None。 使用 Rust 批量解析普通配置站点种子列表,不可用时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.parse_indexer_torrents_fast( 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。 使用 Rust 解析 RSS/Atom 条目,不可用或异常时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.parse_rss_items_fast(xml_text, max_items) 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。 使用 Rust 从标题入口解析 MetaInfo,不可用或异常时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.parse_metainfo_fast(title, subtitle, options or {}) 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。 使用 Rust 从路径入口解析 MetaInfoPath,不可用或异常时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.parse_metainfo_path_fast(path, options or {}) return _moviepilot_rust.parse_metainfo_path_fast(path, options or {})
@@ -138,7 +164,7 @@ def find_metainfo(title: str) -> Optional[dict]:
""" """
使用 Rust 提取标题中的显式媒体标签,不可用或异常时返回 None。 使用 Rust 提取标题中的显式媒体标签,不可用或异常时返回 None。
""" """
if not _moviepilot_rust: if not is_enabled():
return None return None
try: try:
return _moviepilot_rust.find_metainfo_fast(title) 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` - 会在安装 Python 依赖后构建并安装 `moviepilot_rust` 加速扩展,因此本机需要可用的 Rust `cargo`
- 一键安装脚本会自动准备 Rust toolchain 和系统构建工具;手动执行 CLI 安装时,如果未安装 Rust 或本机编译器,请先安装后再执行 `moviepilot install deps` - 一键安装脚本会自动准备 Rust toolchain 和系统构建工具;手动执行 CLI 安装时,如果未安装 Rust 或本机编译器,请先安装后再执行 `moviepilot install deps`
- 如需临时跳过加速扩展构建,可设置 `MOVIEPILOT_SKIP_RUST_ACCEL=1`,但相关核心处理会回退到 Python 实现,性能收益不会生效 - 如需临时跳过加速扩展构建,可设置 `MOVIEPILOT_SKIP_RUST_ACCEL=1`,但相关核心处理会回退到 Python 实现,性能收益不会生效
- 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作
安装前端 release 安装前端 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 import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch
from app.core.context import MediaInfo, TorrentInfo from app.core.context import MediaInfo, TorrentInfo
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
@@ -128,13 +129,40 @@ class TorrentFilterTest(unittest.TestCase):
module._FilterModule__filter_torrents = fail_python_fallback module._FilterModule__filter_torrents = fail_python_fallback
filtered = module.filter_torrents( with patch("app.modules.filter.rust_accel.is_enabled", return_value=True), \
rule_groups=["test"], patch("app.modules.filter.rust_accel.filter_torrents", return_value=[(0, 100)]):
torrent_list=[TorrentInfo(title="Movie", description="")], filtered = module.filter_torrents(
) rule_groups=["test"],
torrent_list=[TorrentInfo(title="Movie", description="")],
)
self.assertEqual(1, len(filtered)) 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): def test_filter_torrent_keeps_extra_filter_semantics(self):
torrent = TorrentInfo( torrent = TorrentInfo(
title="Movie 1080p HDR", title="Movie 1080p HDR",