refactor(startup): centralize resource composition

This commit is contained in:
jxxghp
2026-08-30 12:19:30 +08:00
parent d50177f45d
commit 918baf2b69
6 changed files with 130 additions and 56 deletions

View File

@@ -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()

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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,
)