Archived
refactor: 计费退款不变量收敛为 Billing.scope()(download.py 25 处手工退款 → 0)
为什么:「任何失败路径必须退款」此前靠路由里每条早退/except 手工调 refund_open 维持(download.py 单文件 25 处),上一轮拆 register/login 时已经体会过这个模式的代价——改任何一条返回路径都要核对还原时序, 漏一处不会有测试报警,只剩重启对账兜底在跑。 改动: - Billing.scope()(asynccontextmanager):作用域退出时自动退还仍 open 的账本行。正确性完全依赖 refund_open 的既有幂等口径(只退仍 open 的行)——settle()/rename_to() 消费过的行天然豁免,成功路径零动作, 无需手动「取消自动退」;未捕获异常先退款再原样穿透。 - download.py 四个端点全部改用 async with billing.scope(),删除全部 手工退款与 _fail 收尾闭包;_sync_download_json 的内部退款一并移除 (docstring 标明必须在 scope 内运行)。 - 语义逐路径等价:错误早退→自动退(同原手工);register 成功后 rename→自动退 no-op(同原手工退的 no-op);start 失败行挂 task_id 留给对账(同原);CancelledError 语义不变(退款随取消中断,对账兜底)。 先写测试后动代码:tests/test_billing_scope.py 5 例(TDD 红→绿), 对真实沙箱账本断言——open 行自动退、settle 后 no-op 且余额恰 -1、 rename 后请求级行消失且任务级行保持 open、异常穿透、free 路径零动作。 既有 30 例计费/下载端到端测试作迁移回归网。 门禁:ruff 全过 · pyright 0/0 · pytest 387 passed(382→387)。
This commit is contained in:
1 parent
9a34517454
commit
6fb37534d3
3 files changed
+364
-244
No files matched your search
+21
-1
@@ -12,7 +12,8 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
import auth_system
|
||||
@@ -63,6 +64,25 @@ class Billing:
|
||||
return True
|
||||
return await asyncio.to_thread(auth_system.ledger_rename, self.ledger_key, task_id)
|
||||
|
||||
@asynccontextmanager
|
||||
async def scope(self) -> AsyncGenerator['Billing', None]:
|
||||
"""计费作用域:退出时自动退还仍 open 的账本行。
|
||||
|
||||
把「任何失败路径必须退款」这条不变量从路由的每条早退/except 里
|
||||
收敛到这里。正确性依赖 refund_open 的既有幂等口径(只退仍 open
|
||||
的行),因此:
|
||||
- 错误早退 / 异常转错误响应 → 正常退出作用域 → 自动退;
|
||||
- settle()/rename_to() 已消费的行不在退款范围,成功路径自然豁免,
|
||||
无需手动「取消自动退」;
|
||||
- 未捕获异常先退款再原样穿透,由路由既有 except 转错误响应;
|
||||
- 客户端断连(CancelledError)与迁移前行为一致:退款可能被一并
|
||||
取消,open 行留给重启对账兜底,不额外加 shield 复杂化。
|
||||
"""
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
await self.refund_open('rejected')
|
||||
|
||||
|
||||
async def require_billing(
|
||||
user: SiteUser | None,
|
||||
|
||||
Reference in new issue
Block a user