[Slice 3/4] arise 反应式核:崩溃恢复契约(Crash Recovery / At-most-once) #4

Closed
opened 2026-07-07 02:53:03 +00:00 by KumaAgent · 0 comments
KumaAgent commented 2026-07-07 02:53:03 +00:00 (Migrated from codeberg.org)

Parent

拆自 #1(arise 反应式核 / Reactive Core PRD,Phase 1)。本片是该 PRD 的第 3/4 片,建立在 #3(Slice 2: Self-message Control)之上。

What to build

把 Outbox + Sent Log 从"够用即可"升级为真正满足 ADR-0013 崩溃恢复契约的持久化实现,端到端验证:进程崩溃重启后,不重放、不丢单、不产生孤儿消息。

具体包含:

  • 存储 port 落地为 Postgres:经 nonebot-plugin-orm(SQLAlchemy 2.0 async + Alembic)引入依赖,Outbox(待发队列)+ Sent Log(已发日志)各自建表迁移。不引入 qdrant-client/向量检索(Phase 2 范围)。
  • 崩溃恢复语义(层叠于 Slice 1/2 已有的句柄/send-state 机制之上):
    • core 内部持久写(Outbox 的增删改)包在单一 DB 事务里,崩溃即回滚,等价于"丢弃在途请求,不重放";
    • Sent Log 的写入必须独立于该请求事务、随发即持久——不能和 Outbox 事务共享同一个提交边界,否则"消息已发出但事务未提交就崩溃"会产生 core 认不出的孤儿消息,破坏 at-most-once;
    • 已发到平台的动作不可逆、不回退,崩溃不触发 self_recall
    • 触发批次在请求开始时就 ack(而非处理完才 ack),保证重启后不会对同一批输入再回答一次。

Acceptance criteria

  • 模拟"Sent Log 已写入但 Outbox 事务未提交"时崩溃 → 重启/恢复流程后,已发记录不丢失,且不会对该批输入重新触发一次回答。
  • 模拟"Outbox 事务已提交但从未真正调用发送 API"时崩溃 → 重启后该半成品请求被丢弃,不重放、不重复调用发送 API。
  • 崩溃后不存在"Sent Log 里没有记录、但平台上确实发过"的孤儿消息(Sent Log 写入独立事务这条约束有直接测试覆盖)。
  • 崩溃后不会对已发出的消息触发追溯性的 self_recall(半截回复被接受保留,而非被清理/撤回)。
  • Outbox/Sent Log 的表结构与迁移遵循 nonebot-plugin-orm 标准流程,registry 组织方式为 Phase 2 将追加的画像/事件/knowledge 等表预留空间(不要求提前建那些表,只要求不产生"假设只有这两张表"的隐式耦合)。
  • 崩溃恢复测试可以用受控手段模拟"事务未提交""进程终止"等时序,不依赖真的杀掉进程(比如通过直接操作/回滚数据库事务来模拟)。

Blocked by

  • #3(Slice 2: Self-message Control,需要 Sent Log 已经存在,才能定义其持久化范围与事务边界)
## Parent 拆自 [#1](https://codeberg.org/ProjectKuma/nonebot-plugin-arise/issues/1)(arise 反应式核 / Reactive Core PRD,Phase 1)。本片是该 PRD 的第 3/4 片,建立在 [#3](https://codeberg.org/ProjectKuma/nonebot-plugin-arise/issues/3)(Slice 2: Self-message Control)之上。 ## What to build 把 Outbox + Sent Log 从"够用即可"升级为真正满足 [ADR-0013](https://codeberg.org/ProjectKuma/nonebot-plugin-arise/src/branch/main/docs/adr/0013-crash-recovery-and-side-effect-contract.md) 崩溃恢复契约的持久化实现,端到端验证:进程崩溃重启后,不重放、不丢单、不产生孤儿消息。 具体包含: - 存储 port 落地为 Postgres:经 `nonebot-plugin-orm`(SQLAlchemy 2.0 async + Alembic)引入依赖,Outbox(待发队列)+ Sent Log(已发日志)各自建表迁移。**不引入** `qdrant-client`/向量检索(Phase 2 范围)。 - 崩溃恢复语义(层叠于 Slice 1/2 已有的句柄/send-state 机制之上): - core 内部持久写(Outbox 的增删改)包在**单一 DB 事务**里,崩溃即回滚,等价于"丢弃在途请求,不重放"; - **Sent Log 的写入必须独立于该请求事务、随发即持久**——不能和 Outbox 事务共享同一个提交边界,否则"消息已发出但事务未提交就崩溃"会产生 core 认不出的孤儿消息,破坏 at-most-once; - 已发到平台的动作不可逆、不回退,崩溃不触发 `self_recall`; - 触发批次在请求**开始时**就 ack(而非处理完才 ack),保证重启后不会对同一批输入再回答一次。 ## Acceptance criteria - [ ] 模拟"Sent Log 已写入但 Outbox 事务未提交"时崩溃 → 重启/恢复流程后,已发记录不丢失,且不会对该批输入重新触发一次回答。 - [ ] 模拟"Outbox 事务已提交但从未真正调用发送 API"时崩溃 → 重启后该半成品请求被丢弃,不重放、不重复调用发送 API。 - [ ] 崩溃后不存在"Sent Log 里没有记录、但平台上确实发过"的孤儿消息(Sent Log 写入独立事务这条约束有直接测试覆盖)。 - [ ] 崩溃后不会对已发出的消息触发追溯性的 `self_recall`(半截回复被接受保留,而非被清理/撤回)。 - [ ] Outbox/Sent Log 的表结构与迁移遵循 `nonebot-plugin-orm` 标准流程,registry 组织方式为 Phase 2 将追加的画像/事件/knowledge 等表预留空间(不要求提前建那些表,只要求不产生"假设只有这两张表"的隐式耦合)。 - [ ] 崩溃恢复测试可以用受控手段模拟"事务未提交""进程终止"等时序,不依赖真的杀掉进程(比如通过直接操作/回滚数据库事务来模拟)。 ## Blocked by - [#3](https://codeberg.org/ProjectKuma/nonebot-plugin-arise/issues/3)(Slice 2: Self-message Control,需要 Sent Log 已经存在,才能定义其持久化范围与事务边界)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#4
No description provided.