From 918baf2b69cf4fcdcf7b123fae66b2683276b932 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 30 Aug 2026 12:19:30 +0800 Subject: [PATCH] refactor(startup): centralize resource composition --- app/startup/composition/resource.py | 77 +++++++++++++++++++++++++ app/startup/initializers/modules.py | 16 ++--- app/startup/initializers/resources.py | 57 ++++-------------- docs/rules/05-architecture.md | 3 + tests/test_architecture_dependencies.py | 28 +++++++++ tests/test_managed_resources.py | 5 +- 6 files changed, 130 insertions(+), 56 deletions(-) create mode 100644 app/startup/composition/resource.py diff --git a/app/startup/composition/resource.py b/app/startup/composition/resource.py new file mode 100644 index 000000000..1ad3c3a41 --- /dev/null +++ b/app/startup/composition/resource.py @@ -0,0 +1,77 @@ +"""托管资源 Runtime 与站点资源安装的宿主组合根。""" + +from __future__ import annotations + +import threading +from collections.abc import Callable +from typing import Optional + +from app.adapters.system.resource import ( + ResourceHelper, + configure_resource_version_provider, + reset_resource_version_provider, +) +from app.runtime.capabilities.runtime import CapabilityRuntime +from app.runtime.extensions.resource import ( + AsyncManagedResourceAdapter, + SyncManagedResourceAdapter, + build_managed_resource_registry, +) +from app.runtime.resources import ( + MANAGED_RESOURCE_ASYNC_KIND, + MANAGED_RESOURCE_SYNC_KIND, + configure_managed_resource_runtime, + reset_managed_resource_runtime, +) + +_runtime_lock = threading.RLock() +_managed_resource_runtime: Optional[CapabilityRuntime] = None + + +def configure_managed_resource_composition() -> CapabilityRuntime: + """构建并注入资源 Runtime;只发现声明,不提前启动具体资源。""" + global _managed_resource_runtime + with _runtime_lock: + if _managed_resource_runtime is None: + _managed_resource_runtime = CapabilityRuntime( + build_managed_resource_registry(), + adapters={ + MANAGED_RESOURCE_SYNC_KIND: SyncManagedResourceAdapter(), + MANAGED_RESOURCE_ASYNC_KIND: AsyncManagedResourceAdapter(), + }, + ) + configure_managed_resource_runtime(_managed_resource_runtime) + return _managed_resource_runtime + + +async def stop_managed_resource_composition() -> bool: + """关闭已初始化的资源 Runtime,并返回 owner 是否收敛。""" + with _runtime_lock: + runtime = _managed_resource_runtime + if runtime is None: + return True + return await runtime.shutdown_async(reason="application_shutdown") + + +def reset_managed_resource_composition() -> None: + """释放已关闭的具体 Runtime 及其门面引用。""" + global _managed_resource_runtime + with _runtime_lock: + runtime = _managed_resource_runtime + if runtime is not None and not runtime.is_shutdown: + raise RuntimeError("Managed Resource Runtime 尚未关闭,不能释放 owner") + reset_managed_resource_runtime() + _managed_resource_runtime = None + + +def install_site_resources( + version_provider: Callable[[], tuple[str, str]], +) -> bool: + """装配资源版本读取器并执行一次检测安装,返回是否发生更新。""" + configure_resource_version_provider(version_provider) + return ResourceHelper().check() is True + + +def reset_site_resource_composition() -> None: + """撤销当前 lifespan 的资源版本读取器。""" + reset_resource_version_provider() diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py index 54eb0729d..500446422 100644 --- a/app/startup/initializers/modules.py +++ b/app/startup/initializers/modules.py @@ -17,11 +17,6 @@ except ImportError as e: from app.adapters.external.server import MoviePilotServerHelper from app.adapters.network.doh import DohHelper from app.adapters.system.host import SystemUtils -from app.adapters.system.resource import ( - ResourceHelper, - configure_resource_version_provider, - reset_resource_version_provider, -) from app.application.configuration import ( TransferRetryConfig, configure_transfer_retry_config, @@ -108,6 +103,10 @@ from app.startup.composition.runtime import ( publish_runtime, reset_runtime, ) +from app.startup.composition.resource import ( + install_site_resources, + reset_site_resource_composition, +) from app.startup.composition.security import ( configure_security_access, configure_security_services, @@ -172,7 +171,7 @@ def _module_provider_reset_steps() -> tuple[tuple[str, Callable[[], object]], .. """返回当前模块 lifespan 的完整 Provider 逆序撤销清单。""" return ( ("事件服务", reset_event_services), - ("资源版本 Provider", reset_resource_version_provider), + ("资源版本 Provider", reset_site_resource_composition), ("认证访问服务", reset_security_access), ("Chain 服务", reset_chain_services), ("托管资源引用", reset_managed_resources), @@ -378,8 +377,9 @@ def check_auth(): def update_resources() -> None: """安装可用资源更新,并由组合根统一决定是否重启进程。""" sites_helper = SitesHelper() - configure_resource_version_provider(lambda: (sites_helper.auth_version, sites_helper.indexer_version)) - if ResourceHelper().check() is not True: + if not install_site_resources( + lambda: (sites_helper.auth_version, sites_helper.indexer_version) + ): return restarted, message = SystemHelper.restart() if not restarted: diff --git a/app/startup/initializers/resources.py b/app/startup/initializers/resources.py index bcb39de3d..fb84eb300 100644 --- a/app/startup/initializers/resources.py +++ b/app/startup/initializers/resources.py @@ -1,58 +1,23 @@ -"""Managed Resource 的启动组合与进程关闭入口。""" - -from __future__ import annotations - -import threading -from typing import Optional +"""Managed Resource 的生命周期入口。""" from app.runtime.capabilities.runtime import CapabilityRuntime -from app.runtime.extensions.resource import ( - AsyncManagedResourceAdapter, - SyncManagedResourceAdapter, - build_managed_resource_registry, +from app.startup.composition.resource import ( + configure_managed_resource_composition, + reset_managed_resource_composition, + stop_managed_resource_composition, ) -from app.runtime.resources import ( - MANAGED_RESOURCE_ASYNC_KIND, - MANAGED_RESOURCE_SYNC_KIND, - configure_managed_resource_runtime, - reset_managed_resource_runtime, -) - -_runtime_lock = threading.RLock() -_managed_resource_runtime: Optional[CapabilityRuntime] = None def init_managed_resources() -> CapabilityRuntime: - """构建并注入资源 Runtime;只发现声明,不物化或启动任何资源。""" - global _managed_resource_runtime - with _runtime_lock: - if _managed_resource_runtime is None: - _managed_resource_runtime = CapabilityRuntime( - build_managed_resource_registry(), - adapters={ - MANAGED_RESOURCE_SYNC_KIND: SyncManagedResourceAdapter(), - MANAGED_RESOURCE_ASYNC_KIND: AsyncManagedResourceAdapter(), - }, - ) - configure_managed_resource_runtime(_managed_resource_runtime) - return _managed_resource_runtime + """委托组合根构建并发布托管资源 Runtime。""" + return configure_managed_resource_composition() async def stop_managed_resources() -> bool: - """关闭已初始化的资源 Runtime,并返回 owner 是否收敛。""" - with _runtime_lock: - runtime = _managed_resource_runtime - if runtime is None: - return True - return await runtime.shutdown_async(reason="application_shutdown") + """委托组合根关闭托管资源 Runtime。""" + return await stop_managed_resource_composition() def reset_managed_resources() -> None: - """释放已关闭的具体 Runtime 及其门面引用,不模拟进程内完整重启。""" - global _managed_resource_runtime - with _runtime_lock: - runtime = _managed_resource_runtime - if runtime is not None and not runtime.is_shutdown: - raise RuntimeError("Managed Resource Runtime 尚未关闭,不能释放 owner") - reset_managed_resource_runtime() - _managed_resource_runtime = None + """委托组合根释放已关闭的托管资源 Runtime。""" + reset_managed_resource_composition() diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 2ef766613..4c49d7415 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -170,6 +170,9 @@ network/browser/OCR Adapter assembly. `initializers/site.py` is only the lifecyc hook and must not import or construct those concrete adapters. `startup/composition/cache.py` owns concrete platform cache registration. `initializers/cache.py` is only the startup hook and must not import cache adapters. +`startup/composition/resource.py` owns managed-resource Adapter construction and site +resource installation. `initializers/resources.py` only exposes lifecycle hooks; the +modules initializer retains only the decision to restart after an actual installation. `startup/composition/outbox.py` owns durable handler validation, lazy event/notification replay binding and the short-transaction dispatcher factory. Initializers call this owner in diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index d05b6575e..1e0e17d61 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -2621,6 +2621,34 @@ def test_cache_initializer_delegates_construction_to_composition(): ] assert calls == ["configure_cache_composition"] + +def test_resource_initializer_delegates_construction_to_composition(): + """资源 initializer 只保留生命周期入口,不得构造托管资源 Adapter。""" + initializer_path = APP_ROOT / "startup" / "initializers" / "resources.py" + initializer_tree = ast.parse( + initializer_path.read_text(encoding="utf-8-sig"), + filename=str(initializer_path), + ) + imported_modules = { + node.module + for node in ast.walk(initializer_tree) + if isinstance(node, ast.ImportFrom) and node.module + } + assert imported_modules == { + "app.runtime.capabilities.runtime", + "app.startup.composition.resource", + } + constructors = { + ast.unparse(node.func) + for node in ast.walk(initializer_tree) + if isinstance(node, ast.Call) + } + assert constructors == { + "configure_managed_resource_composition", + "stop_managed_resource_composition", + "reset_managed_resource_composition", + } + def test_system_api_business_orchestration_is_owned_by_application_service(): """System API 只保留传输映射,不得重新拥有日志、设置或更新实现。""" endpoint_path = APP_ROOT / "api" / "endpoints" / "system.py" diff --git a/tests/test_managed_resources.py b/tests/test_managed_resources.py index 279f8c95b..d7d5324b9 100644 --- a/tests/test_managed_resources.py +++ b/tests/test_managed_resources.py @@ -418,15 +418,16 @@ assert "pyvirtualdisplay" not in sys.modules def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None: """未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。""" from app.startup.initializers import resources as managed_resources_initializer + from app.startup.composition import resource as resource_composition build_registry = MagicMock(side_effect=AssertionError("must not discover")) monkeypatch.setattr( - managed_resources_initializer, + resource_composition, "_managed_resource_runtime", None, ) monkeypatch.setattr( - managed_resources_initializer, + resource_composition, "build_managed_resource_registry", build_registry, )