refactor: migrate high-risk write transactions

This commit is contained in:
jxxghp
2026-08-21 20:46:57 +08:00
parent 7de72e0e83
commit b598b516d5
12 changed files with 312 additions and 9 deletions
+4 -1
View File
@@ -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(
+39 -3
View File
@@ -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 编排路径使用。"""
+45
View File
@@ -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
+15
View File
@@ -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]:
"""
+12
View File
@@ -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):
"""
清空插件数据
+16 -1
View File
@@ -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,
))