191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
"""事务控制端口契约。
|
||
|
||
本模块定义 yuxi 三大限界上下文(channels / external_systems / scheduler)
|
||
共享的事务控制端口协议 ``TransactionPort`` 与 ``TransactionContext``,作为
|
||
事务控制的单一事实源(替代各模块自行定义的 ``TransactionPort`` /
|
||
``UnitOfWork`` Protocol)。
|
||
|
||
契约吸收自 ``yuxi.channels.contract.ports.driven.transaction_port`` 的语义,
|
||
并新增 ``savepoint`` 能力(吸收自 ``external_systems`` 的 ``UnitOfWork``)。
|
||
|
||
事务透传机制(C-I1):被驱动适配器(如 ``ChannelPersistenceAdapter`` /
|
||
``ContentReviewRepositoryAdapter`` / ``AgentRunAdapter``)为无状态协议
|
||
转换器,构造时仅注入 ``session_factory``(``Callable[[], AsyncSession]``),
|
||
不持有 session 实例。适配器通过 ``_session_scope(tx)`` 统一管理 session:
|
||
``tx`` 非空时通过 ``tx.get_session()`` 复用应用层主事务 session(C-I1
|
||
透传,``commit=False``,由应用层统一提交);``tx`` 为 ``None`` 时通过
|
||
``session_factory`` 创建独立 session 并自主提交(``commit=True``)。
|
||
|
||
事务边界由应用层独占(§10.1):
|
||
|
||
- 事务边界 **必须** 定义在应用服务层。
|
||
- 领域核心 **不得** 持有事务上下文,**不得** 直接调用事务 API。
|
||
- 被驱动适配器 **不得** 自主开启跨调用的事务,事务范围 **必须**
|
||
由应用层控制;被驱动适配器 **不得** 自主调用 ``commit`` / ``rollback``。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from contextlib import AbstractAsyncContextManager
|
||
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
||
|
||
if TYPE_CHECKING:
|
||
# 仅类型检查期导入,运行时本契约模块不依赖 SQLAlchemy(§6.1)。
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
|
||
@runtime_checkable
|
||
class TransactionPort(Protocol):
|
||
"""事务被驱动端口。
|
||
|
||
提供事务边界控制能力,由应用层在管道或用例编排器中显式调用。
|
||
被驱动适配器通过共享事务上下文(``TransactionContext``)加入同一
|
||
事务,**不得** 自主提交。
|
||
|
||
约束(§10.1):
|
||
- 事务边界 **必须** 定义在应用服务层。
|
||
- 领域核心 **不得** 持有事务上下文,**不得** 直接调用事务 API。
|
||
- 被驱动适配器 **不得** 自主开启跨调用的事务,事务范围 **必须**
|
||
由应用层控制。
|
||
|
||
使用示例::
|
||
|
||
async with transaction.begin() as tx:
|
||
await persistence_port.save_channel_account(cmd, tx=tx)
|
||
await persistence_port.save_audit_log(audit_cmd, tx=tx)
|
||
# 退出 with 块时自动提交,异常时自动回滚
|
||
"""
|
||
|
||
def begin(self) -> TransactionContext:
|
||
"""开启一个新事务,返回事务上下文。
|
||
|
||
@pre
|
||
- 当前无活动事务(或适配器允许嵌套,由实现决定)
|
||
|
||
@post
|
||
- 返回事务上下文,被驱动适配器通过该上下文加入同一事务
|
||
- 上下文管理器退出时自动提交(无异常)或回滚(有异常)
|
||
|
||
@failure
|
||
- TransactionBeginError: 事务开启失败
|
||
|
||
@consistency
|
||
- Strong:事务边界由 ``TransactionContext`` 管理
|
||
"""
|
||
...
|
||
|
||
|
||
class TransactionContext(Protocol):
|
||
"""事务上下文。
|
||
|
||
由 ``TransactionPort.begin()`` 创建,上下文管理器退出时自动提交
|
||
(无异常)或回滚(有异常)。``commit`` / ``rollback`` 幂等,可由
|
||
应用层显式调用;调用后事务对象引用清空,``__aexit__`` 跳过重复
|
||
提交/回滚。
|
||
|
||
事务透传机制(C-I1):无状态适配器通过 ``get_session()`` 获取底层
|
||
共享会话,复用主事务的 session,避免独立提交产生孤儿记录。适配器
|
||
在 ``_session_scope(tx)`` 中判断 ``tx`` 非空且 ``get_session()``
|
||
返回 session 时使用该 session(``commit=False``),否则创建独立
|
||
session(``commit=True``)。
|
||
|
||
``savepoint`` 用于批量操作中隔离每次迭代,避免单条失败导致整个
|
||
会话中毒(``PendingRollbackError``)::
|
||
|
||
async with transaction.begin() as tx:
|
||
for item in items:
|
||
async with tx.savepoint():
|
||
await repo.create(item)
|
||
|
||
被驱动适配器 **不得** 自主调用 ``commit`` / ``rollback``。
|
||
|
||
说明:本 Protocol 未声明 ``@runtime_checkable``,不参与 ``isinstance``
|
||
检查。``TransactionContext`` 仅作为 ``tx`` 参数的类型注解,运行时由
|
||
``TransactionPort.begin()`` 返回的具体实现承载,无需运行时类型校验。
|
||
"""
|
||
|
||
def get_session(self) -> AsyncSession | None:
|
||
"""返回底层共享会话,供无状态适配器复用主事务 session。
|
||
|
||
仅 SQL 实现返回真实 ``AsyncSession``,非 SQL 实现返回 ``None``
|
||
(由适配器回退到 ``session_factory`` 创建独立 session 的路径)。
|
||
|
||
@pre
|
||
- 事务已开启(``__aenter__`` 已调用)
|
||
|
||
@post
|
||
- 返回底层共享会话;非 SQL 实现返回 ``None``
|
||
|
||
@consistency
|
||
- 调用方通过此 session 写入的数据自动加入主事务,
|
||
由应用层统一提交/回滚(C-I1)
|
||
"""
|
||
...
|
||
|
||
async def commit(self) -> None:
|
||
"""提交当前事务(幂等)。
|
||
|
||
仅由应用层(管道或用例编排器)调用,被驱动适配器 **不得** 调用。
|
||
重复调用为 no-op。
|
||
|
||
@pre
|
||
- 事务已开启且未提交/回滚(否则 no-op)
|
||
|
||
@post
|
||
- 事务内所有写操作持久化生效;事务对象引用清空
|
||
|
||
@failure
|
||
- TransactionCommitError: 提交失败(事务自动回滚)
|
||
|
||
@consistency
|
||
- Strong:提交成功后写操作立即可见
|
||
"""
|
||
...
|
||
|
||
async def rollback(self) -> None:
|
||
"""回滚当前事务(幂等)。
|
||
|
||
仅由应用层(管道或用例编排器)调用,被驱动适配器 **不得** 调用。
|
||
重复调用为 no-op。
|
||
|
||
@pre
|
||
- 事务已开启且未提交/回滚(否则 no-op)
|
||
|
||
@post
|
||
- 事务内所有写操作被撤销;事务对象引用清空
|
||
|
||
@failure
|
||
- TransactionRollbackError: 回滚失败(连接异常,需关闭会话)
|
||
|
||
@consistency
|
||
- Strong:回滚后数据库恢复到事务开启前状态
|
||
"""
|
||
...
|
||
|
||
def savepoint(self) -> AbstractAsyncContextManager[None]:
|
||
"""创建 SAVEPOINT,返回 async context manager。
|
||
|
||
进入时创建 SAVEPOINT,正常退出时释放 SAVEPOINT,异常时回滚到
|
||
SAVEPOINT(不污染外层事务)并向上抛出原异常。用于批量操作中
|
||
隔离每次迭代。
|
||
|
||
@pre
|
||
- 外层事务已开启(``__aenter__`` 已调用)
|
||
|
||
@post
|
||
- 正常退出:SAVEPOINT 释放,外层事务继续
|
||
- 异常退出:回滚到 SAVEPOINT,外层事务不受污染,异常向上抛出
|
||
|
||
@consistency
|
||
- Strong:SAVEPOINT 隔离单次迭代失败,不影响外层事务
|
||
"""
|
||
...
|
||
|
||
async def __aenter__(self) -> TransactionContext:
|
||
"""进入事务上下文。"""
|
||
...
|
||
|
||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||
"""退出事务上下文,自动提交(无异常)或回滚(有异常)。"""
|
||
...
|