关于 once
once 是面向 Python 应用的幂等库,用于防止重试、重复投递和并发工作进程重复执行副作用。它结合载荷校验、租约及内存、SQLite 和 PostgreSQL 存储。
主要功能
- 01原子选出唯一执行者
- 02载荷指纹与冲突检测
- 03使用栅栏令牌恢复租约
- 04同步与异步 API
- 05内存、SQLite 和 PostgreSQL 存储
适用场景
- 01防止重试导致支付重复执行
- 02去重 Webhook 或队列消息投递
- 03在多个工作进程间共享幂等状态
- 04异步执行副作用且不阻塞事件循环
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
once
只运行一次任何有副作用的操作——即使 1,000 个调用方同时发起请求。

⚡ once — STORM DEMO
1,000 concurrent attempts to charge order #777 ($49.00)
ACTUAL EXECUTIONS : 1 ← the whole point
served same answer : 1,000 / 1,000
elapsed : 0.1s
💰 double-spend prevented this run: $48,951.00
这不是模拟——这是你现在就能运行的实时攻击:
pip install once-kernel
python -m once.demo
问题
网络会重试。用户会双击。队列会重新投递。AI Agent 会以机器速度重新触发工具。 其中任何一种情况,都可能让一笔付款变成两笔、一封邮件变成三封、一台服务器变成两百台。
大多数团队都会手工实现幂等性表——而其中大多数在并发负载下都会悄悄失效:两个相同的请求都会通过“已经处理过了吗?”检查,然后都执行。漏洞很隐蔽,造成的损失却是真金白银。
once 为所有人一次性把这张表实现正确——一个小巧的幂等性内核,具备手工实现容易遗漏的四项防护:
- 原子选举主执行者 — 并发重复请求无法全部通过检查;只有一个请求会执行,其余请求都会合并获取它的结果。
- 负载指纹(RFC 8785) — 同一个键搭配不同请求体时,会明确报出
IdempotencyConflict,绝不会返回别人的缓存结果。 - 栅栏令牌 + 世代标识 — 崩溃工作进程的租约可以被接管;当“已死亡”的工作进程迟到恢复时,会被锁定在外,无法破坏记录。
- 明确的失败状态 — 失败的尝试会释放键,以便重试;结果未知时,绝不会悄悄重新执行。
使用方法
from once import Once
o = Once()
def charge():
return gateway.charge(order_id="ord_1", amount_cents=4900)
# Retries, double submits, webhook redelivery, agent fan-out → runs ONCE
result = o.run("pay:ord_1", {"order": "ord_1", "amount_cents": 4900}, charge)
一台机器,多个进程,不需要数据库服务器——使用 SQLite,无需安装任何东西:
from once import Once
from once.sqlite import SqliteStore
o = Once(SqliteStore("/var/lib/myapp/once.db")) # schema auto-created
可在重启后继续使用,也能跨进程工作(WAL 模式)。默认的 MemoryStore 两者都不支持——它是按进程隔离的,因此一旦运行第二个工作进程,每个进程都会各自保留一份“哪些操作已经运行过”的记录,保护机制也会悄悄失效。
多台机器——通过你已经在运行的 Postgres 共享状态:
from once import Once
from once.pg import PostgresStore
o = Once(PostgresStore("postgresql://user:pass@host/db")) # table auto-created
异步场景(FastAPI、Agent)——同步副作用会交由工作线程执行,等待者则在事件循环上等待(重复请求风暴下不会耗尽线程池;有测试可以证明这一点):
from once import AsyncOnce
ao = AsyncOnce()
result = await ao.run("pay:ord_1", payload, charge)
你可以信赖的保证
| 如果发生这种情况 | 你会得到 |
|---|---|
| 再次使用相同的键 + 相同的请求负载 | 存储的结果——不会再次执行 |
| 相同的键 + 不同的请求负载 | IdempotencyConflict——绝不会悄悄返回错误结果 |
| 1,000 个并发首次请求 | 一个执行者;其他请求都会合并(wait=True),或收到等待通知 |
| 正在执行的工作进程崩溃 | 租约过期 → 另一个调用方接管 |
| “已死亡”的工作进程迟到恢复 | 被栅栏阻挡——无法完成、无法失败、无法破坏数据 |
| 长时间运行的任务超出租约期限 | heartbeat() 会持续保护它 |
| 你的函数抛出异常 | 键被释放——后续重试可以执行 |
诚实的模型(把这句话贴在海报上):恰好一次执行 + 至少一次结果交付。 真正的网络级恰好一次在物理上不可能实现——声称能做到的库是在骗你。我们只执行一次,并根据请求次数重复交付结果。
像性命攸关一样进行测试
因为这确实事关重大。上面每一项保证都由混沌测试套件验证——通过屏障触发线程风暴、死租约回收踩踏、僵尸写入者栅栏、冻结时钟超时攻击,以及事件循环饥饿检测——每次提交都会同时针对内存存储和真实 PostgreSQL 运行(如果跳过 Postgres 基准测试,CI 会明确失败)。CI 中的安静绝不代表“未经测试”。
我们也在自己的生产邮件发送器中使用它——经过重复审批的发送会重放结果,而不会给真实潜在客户重复发送邮件。先用在自家产品上。
不是什么
- 不是支付服务商——它保护的是你对支付服务商的调用
- 不是工作流引擎(没有 saga、没有多键事务——这是有意的设计选择)
- 不是神奇的“处处恰好一次”——请参阅上文诚实说明的模型
文档
- 示例:FastAPI webhook · Celery 任务——以及真正需要判断的三项决策(键、载荷、存储)
- 5 分钟集成指南
- 完整 API 参考
- 状态机——合法与非法转换
- 我们存储的内容:结果大小 + PII 政策
- 架构决策
同系列项目——EffectFence(Rust)
EffectFence(cargo add effectfence)是同一理念的 Rust 版本:为工具副作用提供因果屏障,并采用内容寻址证书和 MCP 代理模式——effectfence wrap -- <any mcp server> 可为另一个服务的工具调用设置屏障,无需更改任何代码(已通过 once-mcp 验证)。
当副作用发生在 Python 中且你需要持久化存储时,使用 once;当屏障位于 Rust 中或 MCP 服务之前时,使用 EffectFence。
商业支持
免费且采用 Apache-2.0 许可,并将始终如此。如果你希望获得帮助,将其应用到一个已经处理资金的代码库——清点副作用路径、进行风暴测试、设置屏障,并通过 CI 测试确保屏障持续生效——请发送邮件至 [email protected]。详情请见: Fence Audit。
许可证
Apache-2.0
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。