存储边界:每种失控都有硬上限
状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。
队列在数据库里,最大的风险是存储失控。原则:每一种失控都有硬上限,并且看得见,不靠“应该会清理”。
每一行什么时候被删掉
stateDiagram-v2
state events_row {
[*] --> unfanned : trigger / Record
unfanned --> fanned : relay claims it and sets fanned_out_at
unfanned --> poisoned : relay failed on it 5 times
poisoned --> unfanned : events.requeue (Tasks panel)
fanned --> [*] : fanned out and older than 7 days → hourly events retention deletes it
}
state river_job_row {
[*] --> in_progress : relay enqueues (index jobs coalesce per subject within a batch)
in_progress --> completed : success
in_progress --> discarded : permanent failure or attempts exhausted
completed --> [*] : River cleaner deletes after 24 h
discarded --> [*] : River cleaner deletes after 7 days
}
state endpoint {
[*] --> enabled
enabled --> failing : a delivery fails (failing_since set)
failing --> enabled : a delivery succeeds
failing --> disabled : 5 days of continuous failure
disabled --> enabled : owner re-enables
note right of disabled : once disabled the fan-out enqueues no new deliveries to it
}
失控点与控制
| 失控点 | 怎么会失控 | 控制 | 怎么保证 / 看见 |
|---|---|---|---|
events 只增不减 |
没人删;或 relay 卡住、未扇出的行堆积 | 周期任务 events retention 每小时删“已扇出且 ≥ 7 天”的行。未扇出和已隔离的行保留:它们就是积压。 |
总览显示积压数、隔离数、最老未扇出事件的年龄和表大小。未扇出超过 1000 条或最老超过 5 分钟告警 events_backlog;有隔离行告警 events_poisoned。告警在面板上,不发邮件:邮件本身就可能是失败的那一方。 |
river_job 完成行堆积 |
完成的任务行留在表里 | River 清理服务,由选出的主节点按默认值跑:completed 24 小时、discarded 7 天 | 总览显示表大小;存在 discarded 任务时告警 jobs_discarded |
| 死端点重试堆积 | 对方网站长期挂掉 | MaxAttempts 18;failing_since 有值期间新投递排到 5 分钟后;连续失败 5 天自动停用,停用后不再排新投递 |
端点状态与停用原因在后台可见 |
| 批量操作扇出爆炸 | Obsidian 一次导入 2000 篇 | relay 每轮最多 200 行;corpus.index 在同一批内按主体合并。webhook 不合并也不防抖:每条事件是一件事实,给每个订阅它的端点投一次 |
e2e events-bulk-import-bound |
| 空更新也触发事件 | 只刷了 updated_at |
触发器加 WHEN:关心的列真变了才写 |
spec 用“哨兵之前的事件序列”正向断言 |
| 一条坏事件卡死流水线 | relay 处理某条反复出错 | 失败的那批逐行重试,一行出错不阻塞其它行;同一行失败 5 次就隔离并告警;投递失败交给各自任务重试 | 同上,看积压数 |
| 死元组膨胀 | 队列表高频更新,MVCC 旧版本行等 vacuum | events 调低了 autovacuum 阈值(scale factor 0.02)。river_job 保持 River 的设置,因为那份 DDL 归 River。量本来就是一天几千行 |
总览显示表大小 |
积压数、最老事件年龄、表大小和告警都显示在 tasks-panel 的总览里。
先例
访客流量表原本也会无限增长,现在靠每 24 小时一次的保留期任务(visitor traffic retention)兜住;事件和任务表照这个模式来。
换别的方案也躲不开
- Redis 队列同样会堆积,且我们的 Redis 满了是静默淘汰。
- Kafka 自带按时间 / 大小保留,代价是多运维一个服务。
验收
积压 / 表大小在后台可见(e2e tasks-panel、tasks-panel-more);批量导入的上限有自己的 e2e(events-bulk-import-bound);保留期清理和积压告警有 UT。见 events-roadmap · events-test-plan。