feat(governance): unify durable event retention

This commit is contained in:
jxxghp
2026-08-26 12:45:44 +08:00
parent f5dacf79c3
commit 362f606751
29 changed files with 948 additions and 121 deletions
+13 -7
View File
@@ -19,8 +19,9 @@ MoviePilot 保持模块化单体,不把所有后台动作迁到分布式队列
`durable-required` 是目标语义,不代表当前实现已经 durable。ARCH-251 前,Event Registry 中标记该值的
事件仍应在风险报告中说明崩溃窗口。
截至 2026-08-23,宿主正式装配的 `SubscribeAdded``SubscribeModified``SubscribeDeleted`
`SubscribeComplete``DownloadAdded``TransferComplete``TransferFailed` 广播已由业务事务内的 outbox
截至 2026-08-26,宿主正式装配的 `SubscribeAdded``SubscribeModified``SubscribeDeleted`
`SubscribeComplete``DownloadAdded` 以及媒体、字幕、音频的 `TransferComplete` / `TransferFailed`
广播已由业务事务内的 outbox
intent 提供 at-least-once 恢复;订阅完成的历史新增、订阅删除、完成事件和完成统计 intent 同事务提交,
提交后通知/事件/统计仍按原顺序执行,事件与统计失败保持独立 pending。payload 保持插件 dict/对象 ABI
并增加可选幂等键。下载和整理的 outbox 只保存
@@ -38,8 +39,7 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
`DownloadFileDeleted``DownloadDeleted`
- 消息/UI`UserMessage``WebhookMessage``NoticeMessage``MessageAction`
- 生命周期/诊断:`SystemError``ModuleReload``ConfigChanged``WorkflowExecute`
`AgentTokensUsage``MetadataScrape``SubscribeComplete``SubtitleTransferComplete`
`SubtitleTransferFailed``AudioTransferComplete``AudioTransferFailed`
`AgentTokensUsage``MetadataScrape`
- 链式扩展:全部 22 个 `ChainEventType``PluginDataReset``NameRecognize`
`MusicNameRecognize``MediaRecognize``MusicMediaRecognize``AuthVerification`
`AuthIntercept``CommandRegister``TransferRename``TransferRenameBuild`
@@ -50,11 +50,12 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
### E2:业务提交后的用户副作用
- `SubscribeAdded``SubscribeModified``SubscribeDeleted`:订阅业务行 commit 是业务完成点;事件、
- `SubscribeAdded``SubscribeModified``SubscribeDeleted``SubscribeComplete`:订阅业务行 commit 是业务完成点;事件、
通知和服务端上报必须由同事务 durable intent 驱动。ARCH-251 首选 `SubscribeAdded` pilot。
- `DownloadAdded`:下载器确认接收后,下载历史与事件 intent 已在返回前原子提交;通知和模块后处理只在
commit 后启动,事件由 Outbox 恢复投递。
- `TransferComplete``TransferFailed`:整理步骤本身属于 E3但向事件消费者发布结果属于 E2。
- `TransferComplete``TransferFailed` 以及对应的字幕、音频完成/失败事件:整理步骤本身属于 E3
但历史行提交后向事件消费者发布结果属于 E2,使用同一 JSON 快照与恢复 handler。
## 非 Event 后台机制映射
@@ -129,6 +130,11 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
- E0:不重试或仅当前调用内有限重试;队列关停可丢弃,必须记录。
- E1:固定上限或指数退避,下一周期可重建;同一 job key 不并发重叠。
- E2:稳定 idempotency key;原子 claim;指数退避有上限;超过上限进入 dead letter,不无限刷日志。
- Outbox 终态历史并入统一数据维护任务,受 `DATA_CLEANUP_ENABLE` 总开关控制;`completed`
与 dead letter 默认分别保留 30/90 天,并可在高级设置中独立调整或设为 `0` 禁用。
`pending` / `processing` 不参与保留期删除,恰好位于截止边界的记录继续保留。
- 统一数据维护还覆盖全部具备安全时间边界的宿主追加表;Agent 会话保护任务引用,Agent 运行历史
保护运行中与最后一次运行。`transferpending``plugininstallation` 是恢复状态,不按年龄删除。
- E3:步骤级幂等、lease/heartbeat、重启恢复和人工决策入口;外部不可逆步骤必须记录补偿边界。
关停顺序为停止接收新任务、停止 claim、等待有界 drain、释放资源。超过预算的 E2/E3 任务保持持久
@@ -143,6 +149,6 @@ pending 状态交由下次启动,不以取消异常写成成功。
## 验证与演进
- Event Registry 的 `delivery` 字段与本 ADR 同步进入 runtime baseline。
- ARCH-251 已覆盖 Registry 中`durable_required` 事件,并通过 commit 后崩溃、重复 claim、并发
- ARCH-251 已覆盖 Registry 中十一`durable_required` 事件,并通过 commit 后崩溃、重复 claim、并发
claim、JSON 快照恢复和 dead-letter 测试;后续新增 E2 事件必须同时提供业务事务边界和恢复测试。
- ARCH-252 将 Scheduler 的定义、触发和执行状态拆分,但不提升不需要 durable 的 E0 信号。
+7 -1
View File
@@ -419,7 +419,13 @@ flowchart LR
和插件到 Model 的依赖,保证提交权不会被底层抢走。
- **Outbox 可靠副作用**:业务行与 durable intent 在同一 Session/UoW 中提交;提交后由
Outbox dispatcher 依据 topic、claim/lease、有限重试和 dead-letter 执行。完成通知、事件和统计
的 post-commit 逻辑必须保持幂等,不能用普通线程或 TaskRegistry 代替持久 intent。
的 post-commit 逻辑必须保持幂等,不能用普通线程或 TaskRegistry 代替持久 intent。终态历史随统一
数据维护任务分批清理,默认成功记录保留 30 天、dead letter 保留 90 天;总开关和两项保留期由
高级设置维护,待投递和 lease 中记录不参与清理。
- **统一历史保留期**:所有可安全按时间回收的追加型数据均受 `DATA_CLEANUP_ENABLE` 控制,包括消息、
下载及孤儿文件、站点快照、整理历史、下载失败冷却、订阅历史、Agent 会话、Agent 任务运行和 Outbox
终态。Agent 会话会保护任务引用,Agent 运行会保护运行中与最后一次运行;`transferpending`
`plugininstallation` 承担恢复语义,禁止按年龄删除。
- 站点、历史、工作流、Agent 会话删除和插件数据重置已经形成同构事务切片;对应 Application
Command/Service 持有 UoWOper 的 `stage_*` 方法只修改当前会话。插件数据重置从
`startup/initializers/plugins.py` 注入事务能力,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
+5
View File
@@ -453,6 +453,11 @@ Durable post-commit side effects have a separate boundary:
- The dispatcher claims an intent with a lease, executes the topic handler, and
records retry/dead-letter state. Handlers must be idempotent and must not rely
on a live request object.
- Terminal history is part of the shared data-maintenance policy and is cleaned
in bounded daily batches only when that policy is enabled. Completed intents
default to 30-day retention and dead letters to 90 days; both values are
user-configurable and `0` disables that status cleanup. Pending or processing
intents must never be removed by retention cleanup.
- `app/runtime/tasks.py` is only the in-process TaskRegistry boundary. It owns
cancellation and bounded shutdown waiting, but it is not a durable queue and
must not replace an Outbox or persistent task table.
+22 -1
View File
@@ -140,10 +140,31 @@ startup composition supplies the repository, transaction scope and topic
handlers.
The dispatcher claims an intent with a lease, executes an idempotent handler,
and records bounded retries or dead-letter state. The `app/runtime/tasks.py`
and records bounded retries or dead-letter state. The shared data-maintenance
policy controls bounded terminal-history cleanup, with user-configurable 30-day
completed and 90-day dead-letter defaults; `0` disables either cleanup. It must
not delete pending or leased processing rows. The `app/runtime/tasks.py`
TaskRegistry is only the owner for in-process work and bounded shutdown waiting;
it is not a durable queue or a replacement for an Outbox/persistent task table.
All append-only or snapshot history owned by the host must participate in the
shared `DATA_CLEANUP_ENABLE` policy when it has a safe time boundary:
- `message`, `downloadhistory` and orphaned `downloadfiles`, `siteuserdata`,
`transferhistory`, `downloadfailure`, and `subscribehistory` use their own
user-configurable retention periods.
- `agentchat` removes only expired sessions not referenced by an `agenttask`;
`agenttaskrun` removes only expired terminal runs that are neither running nor
the task's current `last_run_id`.
- `outboxmessage` has separate completed and dead-letter retention periods;
pending and processing intents are recovery state and are never age-deleted.
`transferpending` and `plugininstallation` are recovery queues/journals rather
than history. Their age is not proof that they are disposable, so generic
retention cleanup must not delete them. Current-state tables keyed by a user,
site, plugin, workflow, passkey, or media-library item are likewise outside
time-based cleanup; their owning mutation lifecycle must replace or delete them.
Run `./.venv/bin/python scripts/architecture/baseline.py --check-host` after
persistence changes. A deliberate debt reduction may refresh the low-water mark
with `--write-host`; never refresh it to accept newly introduced debt.