Files
MoviePilot/app/startup/composition/database.py
InfinityPacer 4491610adb 修复插件来源仲裁与数据库备份 review 问题
合并 PR #6658。业务相关检查已通过;剩余失败为 PR-Agent 外部 API 日限额与上游既有复杂度 ratchet,不属于本次改动。
2026-09-12 21:22:01 +08:00

261 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据库治理能力的宿主组合根。"""
from __future__ import annotations
from dataclasses import dataclass
from app.adapters.system.backup.database import (
PostgreSQLBackupBackend,
SQLiteBackupBackend,
)
from app.adapters.system.backup.files import BackupFiles
from app.application.backup import BackupPolicy, DatabaseBackupService
from app.application.database import (
DatabaseGovernance,
DatabaseHealthService,
configure_database_governance,
reset_database_governance,
)
from app.application.maintenance import (
DataCleanupService,
read_cleanup_policy,
)
from app.application.plugin.runtime import get_existing_plugin_manager
from app.application.plugin.transaction import (
PluginPersistenceService,
configure_plugin_persistence,
reset_plugin_persistence,
)
from app.application.query import (
DataQueryService,
configure_data_query_service,
reset_data_query_service,
)
from app.application.workflow import (
WorkflowQueryService,
configure_workflow_execution,
configure_workflow_query,
reset_workflow_execution,
reset_workflow_query,
)
from app.db.adapters.pluginidentity import TransactionalPluginIdentityStore
from app.db.adapters.plugininstallation import TransactionalPluginInstallationStore
from app.db.adapters.query import SqlAlchemyDataQueryAdapter
from app.db.adapters.transaction import TransactionalWriteRunner
from app.db.adapters.user import TransactionalUserRepository
from app.db.adapters.workflow import (
TransactionalWorkflowExecutionService,
TransactionalWorkflowQueryRepository,
)
from app.db.engine import get_engine
from app.db.health import probe_database
from app.db.maintenance import DatabaseCleanupRepository
from app.db.oper.systemconfig import SystemConfigOper
from app.db.plugin.registry import database_handles
from app.db.session import (
SessionFactory,
async_session_scope,
)
from app.db.uow import (
configure_transaction_runners,
reset_transaction_runners,
)
from app.db.worker import DatabaseWorker
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
@dataclass(frozen=True, slots=True)
class DatabaseRuntime:
"""保存数据库 worker 与兼容写入口共享的事务执行器。"""
worker: DatabaseWorker
transaction: TransactionalWriteRunner
@dataclass(frozen=True, slots=True)
class DatabaseComposition:
"""保存初始化器后续装配所需的数据库查询能力。"""
data_query: DataQueryService
plugin_persistence: PluginPersistenceService
workflow_query: WorkflowQueryService
_database_runtime: DatabaseRuntime | None = None
_database_runtime_starting = False
async def start_database_runtime() -> DatabaseRuntime:
"""启动进程唯一数据库 worker,并登记兼容写事务 runner。"""
global _database_runtime, _database_runtime_starting
if _database_runtime is not None or _database_runtime_starting:
raise RuntimeError("数据库 runtime 已由当前进程持有")
_database_runtime_starting = True
try:
transaction = TransactionalWriteRunner(
sync_session=SessionFactory,
async_session=async_session_scope,
)
worker = DatabaseWorker()
await worker.start()
configure_transaction_runners(
sync=transaction.sync,
async_=transaction.async_,
)
runtime = DatabaseRuntime(worker=worker, transaction=transaction)
_database_runtime = runtime
return runtime
finally:
_database_runtime_starting = False
async def stop_database_runtime() -> None:
"""关闭数据库 worker;失败时保留 owner 供诊断和重试。"""
global _database_runtime
runtime = _database_runtime
if runtime is not None:
await runtime.worker.shutdown()
_database_runtime = None
def database_runtime_active() -> bool:
"""返回数据库 worker 是否仍由当前进程持有。"""
return _database_runtime is not None
def build_transactional_user_repository() -> TransactionalUserRepository:
"""构造供 Chain、Agent 与进程级认证共享的短会话用户仓储。"""
return TransactionalUserRepository(
sync_session=SessionFactory,
async_session=async_session_scope,
)
def compose_database_services(
*,
runtime: DatabaseRuntime,
system_config: SystemConfigOper,
) -> DatabaseComposition:
"""组合查询、插件持久化与工作流查询所需实现。"""
data_query_adapter = SqlAlchemyDataQueryAdapter(SessionFactory)
data_query = DataQueryService(
subscriptions=data_query_adapter,
histories=data_query_adapter,
async_executor=runtime.worker,
)
plugin_persistence = PluginPersistenceService(
executor=runtime.worker,
identities=TransactionalPluginIdentityStore(SessionFactory),
installations=TransactionalPluginInstallationStore(
SessionFactory,
system_config.update_atomically,
),
)
workflow_query = WorkflowQueryService(
repository=TransactionalWorkflowQueryRepository(
sync_session=SessionFactory,
async_session=async_session_scope,
)
)
return DatabaseComposition(
data_query=data_query,
plugin_persistence=plugin_persistence,
workflow_query=workflow_query,
)
def publish_database_services(composition: DatabaseComposition) -> None:
"""一次性发布当前 lifespan 的查询与插件持久化服务。"""
configure_data_query_service(composition.data_query)
configure_plugin_persistence(composition.plugin_persistence)
configure_workflow_query(composition.workflow_query)
def configure_workflow_execution_composition() -> None:
"""构造并发布使用短事务会话的工作流执行服务。"""
configure_workflow_execution(TransactionalWorkflowExecutionService(SessionFactory))
def reset_workflow_execution_composition() -> None:
"""撤销当前 lifespan 发布的工作流执行服务。"""
reset_workflow_execution()
def reset_database_services() -> None:
"""撤销当前 lifespan 发布的全部数据库派生服务。"""
reset_database_governance()
reset_transaction_runners()
reset_workflow_query()
reset_plugin_persistence()
reset_data_query_service()
def build_database_governance() -> DatabaseGovernance:
"""以缓存同步引擎为事实源构造一个完整数据库治理门面。"""
engine = get_engine()
dialect = engine.dialect.name
if dialect == "sqlite":
backup_backend = SQLiteBackupBackend(engine)
elif dialect == "postgresql":
backup_backend = PostgreSQLBackupBackend(engine)
else:
raise RuntimeError(f"不支持的数据库类型:{dialect}")
def backup_plugin_databases() -> None:
"""为当前已建立的 SQLite 插件库创建独立制品。"""
if dialect != "sqlite":
return
manager = get_existing_plugin_manager()
if manager is None:
return
for handle in database_handles():
if handle.db_path is None:
continue
version = manager.get_local_plugin_version(handle.plugin_id)
if not version:
logger.warning(
"插件 %s 未找到版本,将使用无版本文件名继续备份数据库",
handle.plugin_id,
)
plugin_service = DatabaseBackupService(
backend=SQLiteBackupBackend(handle.engine),
artifact_store_factory=BackupFiles,
policy_reader=lambda: BackupPolicy(
root=get_runtime_setting("DATABASE_BACKUP_PATH") / "plugins" / handle.plugin_id,
retention_days=get_runtime_setting("DB_BACKUP_RETENTION_DAYS"),
max_count=get_runtime_setting("DB_BACKUP_MAX_COUNT"),
),
target=handle.plugin_id,
version=version,
)
plugin_service.create()
return DatabaseGovernance(
health=DatabaseHealthService(probe_database),
cleanup=DataCleanupService(
repository=DatabaseCleanupRepository(session_factory=SessionFactory),
policy_reader=read_cleanup_policy,
),
backup=DatabaseBackupService(
backend=backup_backend,
artifact_store_factory=BackupFiles,
policy_reader=read_backup_policy,
),
plugin_backup=backup_plugin_databases,
)
def configure_database() -> None:
"""构造并登记宿主进程唯一的数据库治理门面。"""
configure_database_governance(build_database_governance())
def read_backup_policy() -> BackupPolicy:
"""读取一次可热更新的数据库备份目录与保留策略。"""
return BackupPolicy(
root=get_runtime_setting("DATABASE_BACKUP_PATH"),
retention_days=get_runtime_setting("DB_BACKUP_RETENTION_DAYS"),
max_count=get_runtime_setting("DB_BACKUP_MAX_COUNT"),
)