跳转至

ADR-0019:一期历史保留期——default 30 天、可配置、两层同一策略

  • 状态:已接受(2026-09-26,由项目负责人决定:保留 1 个月,需要可配置)
  • 相关:ADR-0018(历史分层)、docs/PLAN.md §7.1、§21、附录 B.3;docs/10-retention-deletion-compliance.md §3

背景

retention_class = default 的历史原本“按租户策略逐行 USING TTL”,但租户策略来源从未实现, 代码对 Default 返回“不自动到期”——历史永久保留。ADR-0018 之后历史分两层:Redis 热层 (7 天热窗口)与 ScyllaDB 归档层;两层都需要知道同一个到期时刻,否则会出现一层已删、 一层仍可读,或 Redis 在归档器停摆时永久保留已过期数据。

决策

  1. 一期 default 历史保留 30 天(default_history_retention_days,附录 B.3),部署经 QIM_DEFAULT_HISTORY_RETENTION_DAYS 调整,合法范围 1..=3650(default_history_retention_days_max 约 10 年,低于 ScyllaDB TTL 上限 20 年);非法值拒绝启动。
  2. 到期时刻 = created_at + 保留期,由 qim_store::RetentionPolicy 统一计算:
  3. ephemeral_24h:固定 24 小时(不受本配置影响);
  4. default:本配置;
  5. compliance_hold:永不自动到期,只能由合规流程解除;
  6. tenant_custom:租户策略来源未实现,失败闭合为不自动到期。
  7. 两层同一策略:writer 在进入 COMMITTED 时把到期时刻登记进 Redis 热层到期 GC(与 ephemeral_24h 同一机制,writer 每 5 s 批量取尽);qim-archiver 以剩余期限写 ScyllaDB 逐行 USING TTL(迟到的归档仍按原时刻到期,已过期的记录以 1 秒 TTL 写入、不复活)。两者读同一 环境变量;集成门禁与部署清单必须对两进程设置同一值。
  8. 存储库缺省不自动到期:RetentionPolicy::default() 对 default 返回“不到期”,保留期只能由 装配方显式注入;服务入口(writer、archiver)总是注入配置值(未设即 30 天)。
  9. 一期不做对象存储冷归档与归档回读:保留期 30 天远短于冷归档的收益区间,ScyllaDB 逐行 TTL 即可完成到期删除。保留期将来延长到年级时,冷归档需另立 ADR。

后果

  • 30 天后历史不可读:PULL_HISTORY 返回的页不含已到期记录,earliest_available_conversation_seq 随之前移;邮箱条目本身也按 mailbox_retention_days(30 天)到期,二者对齐。
  • 容量:ScyllaDB 历史驻留 = 日消息量 × 30 天 × 单条大小;Redis 热层仍只驻留 7 天。
  • 调小保留期只影响之后写入的记录:Redis 热层的到期分值与 ScyllaDB 的 TTL 在写入时固化, 已写入的记录按原期限到期。需要立即缩短存量保留期时,须另行执行一次性清理(未实现)。
  • 验收:qim-store/tests/redis_message_store.rs::注入保留期后_default_历史按期到期_合规保留不删、 tiered_message_store.rs::归档按保留策略写逐行_ttl_合规保留不设_ttl、RetentionPolicy 单测。

修订(2026-09-27):热窗口裁剪必须同时移除到期索引条目

分层形态下 default 记录第 7 天先被热窗口裁剪到归档层,第 30 天才到期。原实现裁剪时只删记录与历史索引, 留下了到期 GC 索引(msghistorygc)里的同一 member;到期清理在第 30 天撞上“记录不存在”并按损坏失败, 整批中止且每 5 秒卡在同一条上,索引每条消息约 157 B 只增不减(规划测算时发现,从未在运行中出现)。 修复:trim_hot_history.lua 同时 ZREM 到期索引;gc_history_record.lua 对“记录与索引均缺失且序号不高于 该会话热层下界”的遗留条目只清索引并返回 2,下界之上缺失仍按损坏失败闭合。回归: redis_message_store.rs::热窗口裁剪后到期清理不卡死_升级前遗留的索引条目按下界清掉。

修订(2026-09-28):分层写入方不为“先被裁剪”的消息登记 Redis 到期索引

分层形态下,保留期不短于“热窗口 + 1 天”的消息(默认 30 天 vs 热窗口 7 天)在到期前必然先被 热层裁剪删除,裁剪时连同到期索引一起移除;它们的 msghistorygc 成员从登记到删除从未被到期 GC 用到,却每条驻留约 160 B(2 k msg/s 实测占 core Redis 12%)。现由负责裁剪的写入方 (TieredMessageStore::with_trim → RedisMessageStore::with_trim_window)在进入 COMMITTED 时对这类 消息传 -(不登记);ephemeral_24h 等在热窗口内到期的仍登记。Scylla 侧到期仍由归档写入的逐行 TTL 承担,不受影响。

代价:归档器停摆超过“保留期 − 热窗口 − 1 天”(默认约 22 天)时,已到期记录会滞留 Redis,直到归档 恢复、裁剪继续。此时热层早已停止裁剪、内存持续上涨,告警远早于此触发,不另设兜底。 回归:redis_message_store.rs::分层写入方只为热窗口内到期的消息登记到期索引。