mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: migrate high-risk write transactions
This commit is contained in:
+4
-1
@@ -307,7 +307,10 @@ def get_agent_chat_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> AgentChatService:
|
||||
"""组装 Agent 会话历史查询和删除服务。"""
|
||||
return AgentChatService(repository=_repository("agent_chat", db))
|
||||
return AgentChatService(
|
||||
repository=_repository("agent_chat", db),
|
||||
unit_of_work=_transaction("async", db),
|
||||
)
|
||||
|
||||
|
||||
def get_mediaserver_query_service(
|
||||
|
||||
@@ -45,6 +45,14 @@ class AsyncAgentChatRepository(Protocol):
|
||||
"""删除指定服务端会话。"""
|
||||
...
|
||||
|
||||
async def async_stage_delete(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""暂存删除指定服务端会话,不提交调用方事务。"""
|
||||
...
|
||||
|
||||
def get(self, session_id: str, user_id: Optional[str] = None) -> Optional[Any]:
|
||||
"""同步读取服务端会话。"""
|
||||
...
|
||||
@@ -83,12 +91,29 @@ class AgentChatRecord:
|
||||
messages: list[dict]
|
||||
|
||||
|
||||
class AsyncUnitOfWork(Protocol):
|
||||
"""Agent 会话异步写用例所需的最小事务端口。"""
|
||||
|
||||
async def commit(self) -> None:
|
||||
"""提交当前请求事务。"""
|
||||
...
|
||||
|
||||
async def rollback(self) -> None:
|
||||
"""回滚当前请求事务。"""
|
||||
...
|
||||
|
||||
|
||||
class AgentChatService:
|
||||
"""统一执行 Agent 会话查询、访问控制和删除。"""
|
||||
|
||||
def __init__(self, repository: AsyncAgentChatRepository) -> None:
|
||||
"""保存异步会话持久化端口。"""
|
||||
def __init__(
|
||||
self,
|
||||
repository: AsyncAgentChatRepository,
|
||||
unit_of_work: Optional[AsyncUnitOfWork] = None,
|
||||
) -> None:
|
||||
"""保存会话持久化端口和可选请求级事务。"""
|
||||
self._repository = repository
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
async def list(
|
||||
self,
|
||||
@@ -137,7 +162,18 @@ class AgentChatService:
|
||||
record = await self.get_accessible(session_id, principal)
|
||||
if record is None:
|
||||
return False
|
||||
return await self._repository.async_delete(session_id=session_id)
|
||||
if self._unit_of_work is None:
|
||||
return await self._repository.async_delete(session_id=session_id)
|
||||
try:
|
||||
deleted = await self._repository.async_stage_delete(
|
||||
session_id=session_id
|
||||
)
|
||||
if deleted:
|
||||
await self._unit_of_work.commit()
|
||||
return deleted
|
||||
except Exception:
|
||||
await self._unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
def get_sync(self, session_id: str) -> Optional[AgentChatRecord]:
|
||||
"""同步读取会话投影,供同步 Agent 编排路径使用。"""
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""插件持久化数据写用例。"""
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class PluginDataMutationRepository(Protocol):
|
||||
"""插件数据删除命令所需的无提交仓储端口。"""
|
||||
|
||||
def stage_delete(self, plugin_id: str) -> None:
|
||||
"""暂存目标插件的全部持久化数据删除。"""
|
||||
...
|
||||
|
||||
|
||||
class UnitOfWork(Protocol):
|
||||
"""插件数据同步写用例所需的事务端口。"""
|
||||
|
||||
def commit(self) -> None:
|
||||
"""提交当前逻辑操作。"""
|
||||
...
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""回滚当前逻辑操作。"""
|
||||
...
|
||||
|
||||
|
||||
class DeletePluginDataCommand:
|
||||
"""在一个显式事务中删除目标插件的全部持久化数据。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repository: PluginDataMutationRepository,
|
||||
unit_of_work: UnitOfWork,
|
||||
) -> None:
|
||||
"""保存无提交仓储和事务所有者。"""
|
||||
self._repository = repository
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
def execute(self, plugin_id: str) -> None:
|
||||
"""暂存并提交删除;任一步失败时回滚并传播原异常。"""
|
||||
try:
|
||||
self._repository.stage_delete(plugin_id)
|
||||
self._unit_of_work.commit()
|
||||
except Exception:
|
||||
self._unit_of_work.rollback()
|
||||
raise
|
||||
@@ -314,6 +314,21 @@ class AgentChatOper(DbOper):
|
||||
await AgentChat.async_delete(self._db, chat.id)
|
||||
return True
|
||||
|
||||
async def async_stage_delete(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""暂存 Agent 会话删除并 flush,不提交请求级事务。"""
|
||||
if not isinstance(self._db, AsyncSession):
|
||||
raise RuntimeError("Agent 会话暂存删除需要调用方提供 AsyncSession")
|
||||
chat = await self.async_get(session_id=session_id, user_id=user_id)
|
||||
if not chat:
|
||||
return False
|
||||
await self._db.delete(chat)
|
||||
await self._db.flush()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def to_summary(chat: AgentChat) -> dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.plugindata import PluginData
|
||||
|
||||
@@ -82,6 +85,15 @@ class PluginDataOper(DbOper):
|
||||
else:
|
||||
PluginData.del_plugin_data(self._db, plugin_id)
|
||||
|
||||
def stage_delete(self, plugin_id: str) -> None:
|
||||
"""暂存目标插件全部数据删除并 flush,不提交调用方事务。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("插件数据暂存删除需要调用方提供 Session")
|
||||
self._db.execute(
|
||||
delete(PluginData).where(PluginData.plugin_id == plugin_id)
|
||||
)
|
||||
self._db.flush()
|
||||
|
||||
def truncate(self):
|
||||
"""
|
||||
清空插件数据
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.runtime.extensions.plugin_manager import (
|
||||
)
|
||||
from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult
|
||||
from app.application.plugin.catalog import PluginCatalogService
|
||||
from app.application.plugin.data import DeletePluginDataCommand
|
||||
from app.adapters.external.plugin.client import PluginMarketClient
|
||||
from app.runtime.extensions.plugin.storage import (
|
||||
PluginStorage,
|
||||
@@ -41,6 +42,8 @@ from app.adapters.system.plugin.package import PluginPackageManager
|
||||
from app.adapters.system.host import SystemUtils
|
||||
from app.db.oper.plugindata import PluginDataOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.session import SessionFactory
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
from app.runtime.log import logger
|
||||
from app.foundation.version import compare_version
|
||||
from app.schemas.plugin import PluginRuntimeStatus
|
||||
@@ -52,6 +55,18 @@ async def _async_write_plugin_config(key, value):
|
||||
return await SystemConfigOper().async_set(key, value)
|
||||
|
||||
|
||||
def _delete_plugin_data(plugin_id: str) -> None:
|
||||
"""用独占同步会话执行插件重置的数据删除事务。"""
|
||||
session = SessionFactory()
|
||||
try:
|
||||
DeletePluginDataCommand(
|
||||
repository=PluginDataOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
).execute(plugin_id)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def _prepare_legacy_plugin_import(*, plugin_id: str, plugin_dir: Path) -> None:
|
||||
"""在执行旧插件顶层代码前准备其静态导入所需的宿主资源。"""
|
||||
for capability_id in scan_plugin_resource_imports(plugin_id, plugin_dir):
|
||||
@@ -98,7 +113,7 @@ def configure_plugin_services() -> None:
|
||||
write=lambda key, value: SystemConfigOper().set(key, value),
|
||||
async_write=_async_write_plugin_config,
|
||||
delete=lambda key: SystemConfigOper().delete(key),
|
||||
delete_data=lambda plugin_id: PluginDataOper().del_data(plugin_id),
|
||||
delete_data=_delete_plugin_data,
|
||||
))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user