Python库用状态机保证幂等性,包裹支付/发邮件等副作用后,重试、恢复、重放均只执行一次。
包装支付操作。它在重试、崩溃、恢复和重放中都只执行一次。
exactly-once 是一个 Python 库,让副作用只执行一次。包装支付发票、发送邮件或提交交易的函数,它会按 key 执行一次,之后每次调用都回放已存储的结果。
完整集成如下:
from exactly_once import once, Store, current_key
store = Store.sqlite("effects.db")
@once(store, key=lambda inv, **_: f"pay:{inv.id}")
def pay_invoice(inv):
return payments.transfer(inv.vendor, inv.amount, idempotency_key=current_key())
调用 pay_invoice(invoice) 会向供应商完成支付。从重试、恢复的运行、重放或第二个 worker 再次调用时,它返回已记录的结果。供应商只收到一次付款。
它所应对的崩溃场景
Agent 支付一张发票。转账到达支付提供商并成功。但在提供商返回 200 OK 和记录结果的那一行之间,进程死掉了。Agent 重启后再次到达同一步骤。
exactly-once 在 Agent 进入调用的瞬间就写入一条记录。pay_invoice 声明 key pay:{invoice.id},Store 将其标记为 IN_FLIGHT。当结果返回时,Store 将其标记为 COMMITTED 并保存该结果。崩溃之后,记录显示 IN_FLIGHT 且结果为空——库知道支付已启动,但没有证据表明它完成了。
因此它会将该 key 隔离。Agent 将那笔支付留待决策,继续处理。你给 @once 传入一个 prober,让它向支付 API 查询是否存在使用该 idempotency key 的转账:库在提供商确认支付后提交 key,在提供商确认没有时释放它。在答案到来之前,被搁置的支付留在账本中,你可以看到它:
store.list(state="in_flight") # every payment awaiting a verdict
保证是如何成立的
三个状态,一个原子操作:
FRESH ──claim──▶ IN_FLIGHT ──commit──▶ COMMITTED
claim(key) 是一个原子的检查并设置操作。第一个声明某个 key 的调用者赢得它并执行效果;其他所有调用者读取已存在的记录。整个保证建立在这一个操作之上,因此它存在于你选择的 store 中,每个 store 都会说明它所强制的作用域:
保证只有一行:一个受保护的效果每个 key 最多进入一次。这就是 exactly-once 效果执行——最多一次,然后回放——它跨越重试、并发 worker、崩溃和重放而成立。将 current_key() 传递给提供商将其端到端延伸:你的 store 阻止 Agent 两次进入调用,而提供商的 idempotency key 将重复请求解析为同一笔支付。
以支付的标识作为 key
key 是效果的标识。pay:{invoice.id} 将保护绑定到发票,因此同一张发票始终解析到同一个受保护的支付,每张独立的发票获得自己的 key。以业务标识作为 key——发票 ID、订单 ID——Store 会对所有共享它的尝试去重。
@once 装饰器和 with once(...) 块支持同步和异步,语义相同。
Store 支持内存、SQLite、Redis 和 Postgres,每种都有文档化的原子性和写入模型。
崩溃时隔离,配合 probers 和可选的 worker lease 进行恢复。
通过 current_key() 直通提供商的 idempotency key。
一个链上适配器,以交易的 nonce 作为 key,使恢复后的 Agent 只提交一次。
为 LangGraph 节点和 CrewAI 工具运行提供包装器,以及任何普通函数、循环或 worker。
可审查的账本:store.list(state=...) 供给仪表盘、审计和对账。
零外部依赖,完整类型标注,无需 LLM。
观察它在 unguarded Agent 放行一笔重复支付时拦截一笔重复支付,并排对比:
python examples/crash_mid_payment.py
然后在你的 Agent 中包装第一个不可逆的调用——那笔重复最难解释的支付。
pip install exactly-once
github.com/swarmproof/exactly-once · MIT
延伸阅读:
Stripe on idempotency
AWS Lambda Powertools idempotency utility
You cannot have exactly-once delivery