128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""M11b-2 运行时单事务写入 —— 异常定义(唯一真源)。
|
||
|
||
设计要点(对应需求点 3「错误处理:事务失败时回滚并抛出明确异常,不吞错误」):
|
||
1. 所有异常统一继承 :class:`PblRuntimeError`,携带机器可判定的 ``code`` 与结构化 ``detail``;
|
||
2. 事务失败一律「先 ROLLBACK、再抛出」,原始异常通过 ``raise ... from exc`` 保留 cause;
|
||
3. 并发冲突(乐观锁 / 行锁下版本被他人推进)单独成类 :class:`ConcurrentStateConflict`,
|
||
让上层可以区分「可重试」与「真失败」,而不是笼统的 500。
|
||
|
||
本模块零依赖(只用标准库),不引入广播 / 轮询相关任何东西。
|
||
|
||
M11b-2a 迁移补充(响应 QC 退回意见 #1「补齐异常别名」):宿主挂载层
|
||
``world_sync/init.py`` 与已迁移的测试用例使用一组**语义别名**
|
||
(``RuntimeWriteError`` / ``InvalidWriteRequest`` / ``ConnectionUnavailable`` /
|
||
``TransactionAborted``)。别名与真源类是**同一个类对象**(``is`` 相等),
|
||
因此 ``except`` 与 ``assertRaises`` 在两套名字下行为完全一致,不存在双真源。
|
||
"""
|
||
|
||
__all__ = [
|
||
"PblRuntimeError",
|
||
"TxConfigError",
|
||
"ConnFactoryNotConfigured",
|
||
"EventWriteError",
|
||
"StateWriteError",
|
||
"ConcurrentStateConflict",
|
||
"TxAbortError",
|
||
# --- M11b-2a 语义别名(与上方真源同一对象,供宿主层/测试层使用)---
|
||
"RuntimeWriteError",
|
||
"InvalidWriteRequest",
|
||
"ConnectionUnavailable",
|
||
"TransactionAborted",
|
||
]
|
||
|
||
|
||
class PblRuntimeError(Exception):
|
||
"""M11b-2 运行时写入异常基类。
|
||
|
||
:param message: 人读信息
|
||
:param detail: 结构化上下文(tenant/world/entity/version/cause 等),便于日志与断言
|
||
"""
|
||
|
||
code = "PBL_RUNTIME_ERROR"
|
||
http_hint = 500
|
||
|
||
def __init__(self, message="", **detail):
|
||
super(PblRuntimeError, self).__init__(message or self.__class__.__doc__ or "")
|
||
self.message = message or ""
|
||
self.detail = dict(detail)
|
||
|
||
def to_dict(self):
|
||
"""机器可读形式,供接口层 / 测试断言使用。"""
|
||
return {
|
||
"code": self.code,
|
||
"message": self.message,
|
||
"detail": self.detail,
|
||
"http_hint": self.http_hint,
|
||
}
|
||
|
||
# 兼容旧调用名(init.py / 测试用 as_dict())
|
||
as_dict = to_dict
|
||
|
||
def __str__(self):
|
||
if self.detail:
|
||
parts = ", ".join(
|
||
"%s=%r" % (k, self.detail[k]) for k in sorted(self.detail.keys())
|
||
)
|
||
return "[%s] %s {%s}" % (self.code, self.message, parts)
|
||
return "[%s] %s" % (self.code, self.message)
|
||
|
||
|
||
class TxConfigError(PblRuntimeError):
|
||
"""配置/参数错误(缺字段、方言不认识、配置文件不可解析)。属于「调用方错误」。"""
|
||
|
||
code = "PBL_TX_CONFIG_ERROR"
|
||
http_hint = 500
|
||
|
||
|
||
class ConnFactoryNotConfigured(PblRuntimeError):
|
||
"""fail-closed:三级优先级都拿不到 conn_factory 时抛出,绝不偷偷连库。"""
|
||
|
||
code = "PBL_TX_CONN_FACTORY_MISSING"
|
||
http_hint = 500
|
||
|
||
|
||
class EventWriteError(PblRuntimeError):
|
||
"""pbl_runtime_event 插入失败(整事务已回滚)。"""
|
||
|
||
code = "PBL_RUNTIME_EVENT_WRITE_FAILED"
|
||
http_hint = 500
|
||
|
||
|
||
class StateWriteError(PblRuntimeError):
|
||
"""pbl_entity_state 更新/写入失败(整事务已回滚)。"""
|
||
|
||
code = "PBL_ENTITY_STATE_WRITE_FAILED"
|
||
http_hint = 500
|
||
|
||
|
||
class ConcurrentStateConflict(PblRuntimeError):
|
||
"""并发冲突:乐观锁 expected_version 不匹配,或 UPDATE 影响行数 != 1。
|
||
|
||
出现该异常意味着「不出现脏写/丢失更新」这条验收标准被守住了:
|
||
后到的事务被拒绝,而不是覆盖先到的写入。上层可重读版本后重试。
|
||
"""
|
||
|
||
code = "PBL_ENTITY_STATE_CONFLICT"
|
||
http_hint = 409
|
||
|
||
|
||
class TxAbortError(PblRuntimeError):
|
||
"""事务在提交阶段失败(commit 抛错),已执行 ROLLBACK。"""
|
||
|
||
code = "PBL_TX_ABORTED"
|
||
http_hint = 500
|
||
|
||
|
||
# ===========================================================================
|
||
# M11b-2a 语义别名 —— 同一个类对象,不是新异常族(避免双真源)
|
||
# RuntimeWriteError → PblRuntimeError(基类,宿主层统一 except 用)
|
||
# InvalidWriteRequest → TxConfigError(调用方参数/配置错误)
|
||
# ConnectionUnavailable → ConnFactoryNotConfigured(拿不到连接工厂,fail-closed)
|
||
# TransactionAborted → TxAbortError(提交阶段失败,已回滚)
|
||
# ===========================================================================
|
||
RuntimeWriteError = PblRuntimeError
|
||
InvalidWriteRequest = TxConfigError
|
||
ConnectionUnavailable = ConnFactoryNotConfigured
|
||
TransactionAborted = TxAbortError
|