跳转至

ADR-0023:离线消息(邮箱)保留 7 天、历史 30 天;新设备从保留窗口拉取离线消息

  • 状态:已接受(2026-09-28,项目负责人:“离线消息 7 天,历史消息 30 天。离线消息的定义是新客户端 登录后,至少能拉回 7 天的消息,再多的消息需要利用历史消息拉取”)
  • 修订:docs/PLAN.md 附录 B.3 mailbox_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 (逐会话翻完全部历史)。两者都不符合负责人给出的离线消息定义。

决策

  1. mailbox_retention_days = 7(契约常量 MAILBOX_RETENTION_DAYS,原 30)。日桶按 UTC 日绝对到期: day D 的条目在 (D + 7 + 1) × 86400 s 到期,任何条目至少保留 7 整天。Redis、ScyllaDB、Memory 三个后端统一引用该常量,qim-store 不再重复定义。
  2. 历史保留仍为 30 天(default_history_retention_days,ADR-0019 不变)。离线消息过期后降级为 历史消息(§9.5.1),正文仍可按会话 PULL_HISTORY 读取。
  3. 新设备(零游标)从保留窗口拉取离线消息:last_applied_mailbox_seq 原值为 0 表示“从未应用 过任何条目”,PULL_MAILBOX 对它不判过期,返回保留窗口内尚存的全部条目(显式 trim 之后的, 至少 7 天);更早的消息由客户端按会话 PULL_HISTORY 按需获取。零游标没有本地状态,窗口之前的 缺口不属于它,无需 CURSOR_EXPIRED 暴露。
  4. 非零游标越过过期边界仍返回 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,由契约常量驱动。