ForcePilot/backend/package/yuxi/channels/adapters/sqlalchemy_transaction_adapter.py

147 lines
5.9 KiB
Python
Raw Normal View History

"""SQLAlchemy 事务适配器。
实现 ``TransactionPort`` 契约基于 SQLAlchemy ``AsyncSession`` 提供事务
边界控制能力事务边界由应用层管道或用例编排器显式调用被驱动适配
器通过 ``TransactionContext`` 透传C-I1加入同一事务**不得** 自主提交
事务共享机制``create_driven_adapters(db, session_factory, ...)`` 将同一
请求级 ``AsyncSession`` 注入到 ``ConversationAdapter`` /
``SqlAlchemyTransactionAdapter``请求级事务边界适配器共享 ``db`` 保证
事务一致性``ChannelPersistenceAdapter`` 为无状态适配器注入
``session_factory``通过 ``_session_scope(tx)`` 按需获取 session
``tx`` 非空时复用 ``tx.get_session()`` 返回的请求级 ``db``加入应用层
事务``tx`` ``None`` 时通过 ``session_factory()`` 创建独立 session
并自主提交
"""
from __future__ import annotations
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.channels.contract.ports.driven.transaction_port import (
TransactionPort,
)
__all__ = ["SqlAlchemyTransactionAdapter", "SqlAlchemyTransactionContext"]
class SqlAlchemyTransactionContext:
"""SQLAlchemy 事务上下文。
封装 ``AsyncSession`` 与其事务被驱动适配器在构造时共享同一
``AsyncSession````begin()`` 在共享 session 上开启事务所有适配器
的写操作自动加入上下文管理器退出时自动提交无异常或回滚
有异常
被驱动适配器 **不得** 调用 ``commit`` / ``rollback``仅由应用层
通过 ``TransactionPort`` 控制
"""
def __init__(self, session: AsyncSession) -> None:
"""初始化事务上下文。
Args:
session: SQLAlchemy 异步会话事务边界由本上下文控制
"""
self._session = session
self._txn: Any = None
def get_session(self) -> AsyncSession:
"""返回底层共享 ``AsyncSession``,供未在构造时共享 session 的适配器复用主事务。
C-I1``AgentRunAdapter`` 等适配器在构造时未与事务适配器共享 session
通过本方法获取主事务的共享 session复用同一事务避免独立提交产生
孤儿记录
"""
return self._session
async def commit(self) -> None:
"""提交当前事务。
仅由应用层调用被驱动适配器 **不得** 调用提交后清空事务对象
避免 ``__aexit__`` 重复提交
"""
if self._txn is not None:
await self._txn.commit()
self._txn = None
async def rollback(self) -> None:
"""回滚当前事务。
仅由应用层调用被驱动适配器 **不得** 调用回滚后清空事务对象
避免 ``__aexit__`` 重复回滚
"""
if self._txn is not None:
await self._txn.rollback()
self._txn = None
async def __aenter__(self) -> SqlAlchemyTransactionContext:
"""进入事务上下文,开启 SQLAlchemy 事务。
SQLAlchemy 2.0 autobegin 语义下前置读操作 ``getChannelSessionByPeer``
SELECT 查询会在 session 上隐式开启只读事务若不处理
``begin()`` 会抛 ``InvalidRequestError: A transaction is already
begun on this Session.``
此处检测并提交隐式事务后再开启显式事务安全性保证按适配器契约
``tx=None`` 的写操作自主提交autobegin 事务仅含读操作提交不会
产生副作用数据落库应用层显式事务边界§10.1由此方法独占控制
"""
if self._session.in_transaction():
await self._session.commit()
self._txn = await self._session.begin()
return self
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
"""退出事务上下文。
若事务未被显式提交/回滚``self._txn`` 非空且 ``is_active``则按
异常状态自动提交无异常或回滚有异常若已被显式提交/回滚
``self._txn`` 为空或已被 SQLAlchemy 内部关闭``is_active=False``
flush 失败自动关闭事务则跳过避免 ``ResourceClosedError``
事务对象退出后释放引用避免泄漏
"""
try:
if self._txn is not None and self._txn.is_active:
if exc is None:
await self._txn.commit()
else:
await self._txn.rollback()
finally:
self._txn = None
class SqlAlchemyTransactionAdapter(TransactionPort):
"""SQLAlchemy 事务适配器。
实现 ``TransactionPort`` 契约基于共享的 ``AsyncSession`` 提供事务
边界控制事务边界由应用层显式调用 ``begin()`` 开启被驱动适配器
通过构造时共享的 session 加入同一事务
关键约束
- 事务边界 **必须** 由应用层控制
- 被驱动适配器 **不得** 自主调用 ``commit`` / ``rollback``
- 事务范围 **必须** 由应用层显式声明
"""
def __init__(self, session: AsyncSession) -> None:
"""初始化事务适配器。
Args:
session: SQLAlchemy 异步会话与被驱动适配器共享以保证事务
一致性
"""
self._session = session
def begin(self) -> SqlAlchemyTransactionContext:
"""开启一个新事务,返回 SQLAlchemy 事务上下文。
Returns:
SQLAlchemy 事务上下文被驱动适配器通过构造时共享的 session
加入同一事务上下文管理器退出时自动提交无异常或回滚
有异常
"""
return SqlAlchemyTransactionContext(self._session)