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