ADR-0023:离线消息(邮箱)保留 7 天、历史 30 天;新设备从保留窗口拉取离线消息¶
- 状态:已接受(2026-09-28,项目负责人:“离线消息 7 天,历史消息 30 天。离线消息的定义是新客户端 登录后,至少能拉回 7 天的消息,再多的消息需要利用历史消息拉取”)
- 修订:
docs/PLAN.md附录 B.3mailbox_retention_days、§9.5 离线/历史对照表、§9.6.1 新设备冷启动; ADR-0019(历史 30 天不变)
背景¶
一期口径(docs/08 §4.3)日消息 2,000 万、平均扇出 R_avg ≈ 22,即每天约 4.4 亿条邮箱条目。按实测 Redis 单价 296 B/条,30 天驻留约 3.9 TB,7 天约 0.9 TB;邮箱保留期是邮箱层容量的线性因子。
原 §9.6.1 规定新设备以空游标登录时不回填任何邮箱事件,只拉会话列表、逐会话拉最近一页历史。
而实现中新设备游标为 0,拉取时只要该用户有已过期日桶,就被判 CURSOR_EXPIRED 走 REBUILD
(逐会话翻完全部历史)。两者都不符合负责人给出的离线消息定义。
决策¶
mailbox_retention_days= 7(契约常量MAILBOX_RETENTION_DAYS,原 30)。日桶按 UTC 日绝对到期: day D 的条目在(D + 7 + 1) × 86400 s到期,任何条目至少保留 7 整天。Redis、ScyllaDB、Memory 三个后端统一引用该常量,qim-store 不再重复定义。- 历史保留仍为 30 天(
default_history_retention_days,ADR-0019 不变)。离线消息过期后降级为 历史消息(§9.5.1),正文仍可按会话PULL_HISTORY读取。 - 新设备(零游标)从保留窗口拉取离线消息:
last_applied_mailbox_seq原值为 0 表示“从未应用 过任何条目”,PULL_MAILBOX对它不判过期,返回保留窗口内尚存的全部条目(显式 trim 之后的, 至少 7 天);更早的消息由客户端按会话PULL_HISTORY按需获取。零游标没有本地状态,窗口之前的 缺口不属于它,无需CURSOR_EXPIRED暴露。 - 非零游标越过过期边界仍返回
CURSOR_EXPIRED→ REBUILD(§9.6.3 不变):该设备曾应用过窗口 之前的条目,中间缺口必须显式暴露,禁止静默跳过(§9.3 规则 3)。
后果¶
- 一期邮箱驻留降为原来的 7/30:R_avg ≈ 22 时约 0.9 TB(Redis)。它仍超出单台 standalone Redis 的 合理规模,一期邮箱存储选型(Redis 或 ScyllaDB)以群负载实测为准,另行决定。
- 离线超过 7 天的设备必然 REBUILD;新设备登录拉取量上限为 7 天的邮箱条目。
- 升级时已有日桶按旧 30 天到期写入,新代码对同一日桶按 7 天校验到期时刻会失败闭合(Redis/Scylla 均拒绝同桶异到期)。当前无生产数据,升级前清空邮箱数据;有数据时须离线迁移。
验收¶
- 三个后端共享用例:零游标在存在过期日桶时返回全部存活条目、不返回
CURSOR_EXPIRED;非零游标 低于过期边界仍返回CURSOR_EXPIRED;零游标不返回显式 trim 之前的条目。 - Redis/Scylla 日桶到期时刻 =
(D + mailbox_retention_days + 1) × 86400 s,由契约常量驱动。