跳转至

Q-IM 消息持久化与完整客户端 TestPlan

日期:2026-08-23
范围:main、store/redis、store/scylla,以及 qim-sdk / qim-desktop / qim-gui
契约来源:docs/PLAN.md、docs/08-test-and-capacity-plan.md、docs/11-client-sdk.md、ADR-0007/0008/0010、BRANCHES.md

1. 完成定义

本轮不是以“测试能过”作为完成,而是同时满足以下可观测结果:

  1. SEND_ACK 只在 MessageRecord 与可靠 Outbox 均已提交后返回。
  2. writer、FanoutCoordinator、MailboxNode、Redis、ScyllaDB 或客户端在任一声明的故障点重启后,已确认消息最终仍能出现在历史和全部目标用户邮箱中。
  3. 同一 client_message_id、Outbox 事件或分发事件被重复处理时,只产生一条逻辑消息;允许至少一次传输,不允许重复气泡或新分配邮箱坐标。
  4. main 与 store/redis 的 Redis 后端、store/scylla 的 ScyllaDB 后端通过同一份存储契约和 E2E 用户流程。
  5. GUI 与 CLI 默认使用稳定的磁盘本地库;应用退出重启后,会话、历史、已读水位与 pending 可恢复。
  6. 客户端覆盖服务端一期已经实现的公开能力;服务端未实现的媒体、E2EE、离线推送等能力明确呈现为“不支持”,不伪造成功。
  7. 功能门禁、故障恢复门禁和性能门禁均在隔离环境中执行,不能通过“依赖不可用即跳过”得到绿色结果。

2. 子模块能力与边界

2.1 MessageCommitter(ConversationWriter 提交边界)

Capability

  • 输入:已完成认证、成员校验、审核与限流的发送请求。
  • 输出:CommittedMessage 或明确错误。
  • 保证:幂等占位、坐标固化、MessageRecord 写入、Outbox 发布、状态转 COMMITTED 的顺序可恢复。
  • SEND_ACK 只能由成功的 CommittedMessage 生成。

Boundary

  • 负责正文与 Outbox 的提交,不负责逐用户邮箱物化、在线推送和 UI 状态。
  • 不在调用方暴露 Redis key、CQL 表、Kafka offset 等后端细节。
  • 群成员快照/版本作为 Outbox 的确定性输入保存,恢复时不得重新猜测。

Forbidden Zone

  1. 禁止在 MessageRecord 或 Outbox 提交前发送 SEND_ACK。
  2. 禁止把 dedup 首次占位直接写成 COMMITTED。
  3. 禁止重复请求只回 ACK 而不恢复未完成的提交步骤。
  4. 禁止接受空、非 16 字节或全零 client_message_id 并降级为非幂等发送。
  5. 禁止用客户端时间或随机数生成任何服务端排序/恢复坐标。

2.2 CommitLog 与 FanoutCoordinator

Capability

  • Outbox 使用 Redpanda/Kafka API 的持久 topic。
  • FanoutCoordinator 以 read_committed 消费 Outbox,按目标 MailboxShard 生成稳定 dispatch_id 的分发事件。
  • 产出分发事件与提交 Outbox 消费位点在同一事务中完成。

Boundary

  • 不写用户邮箱,不直接写客户端 Socket。
  • 不重新解释正文,不修改 MessageRecord。
  • 分区键和 dispatch 拆分只由稳定路由规则决定。

Forbidden Zone

  1. 禁止把进程内 mpsc/TCP 写成功视为可靠 Outbox。
  2. 禁止自动消费者组 rebalance 改变 MailboxShard 的静态归属。
  3. 禁止在分发事件可靠提交前提交 Outbox 消费位点。
  4. 禁止使用随机 dispatch_id 或重试时重新计算不同的成员集合。
  5. 禁止将 producer/consumer 的后端异常原样暴露成客户端协议细节。

2.3 Mailbox 分发日志消费与物化

Capability

  • 从分发日志记录的 partition/offset 构造稳定 mailbox_seq。
  • 同一 dispatch 重放复用同一 mailbox_seq、event_id 和 created_at。
  • 全部收件人可靠落盘后推进连续水位;失败时不越过空洞。
  • 接管起点取各 lane 水位最小值之后,而不是消费者组已提交 offset。

Boundary

  • 负责邮箱引用、连续水位、投影事件与在线推送。
  • 正文内联判定只执行 writer/Outbox 已给出的决定,不自行重算。
  • 在线推送允许丢,个人邮箱不允许丢。

Forbidden Zone

  1. 禁止重放 dispatch 时重新分配新的 mailbox offset。
  2. 禁止物化失败后销账或推进水位。
  3. 禁止依赖只含 offset、没有原始分发事件的本地 pending 集合恢复。
  4. 禁止按日期桶拼接后直接推进 covered_through_seq。
  5. 禁止吞掉条目解码错误并让客户端游标跨过损坏记录。

2.4 Redis MessageStore / MailboxStore

Capability

  • 提供与 MessageStore、MailboxStore 契约一致的 Redis 实现。
  • 保证跨日期桶全局按 mailbox_seq 排序、事件组不可切分、裁剪与 CURSOR_EXPIRED 闭环。
  • 仅接受 standalone Redis,且生产配置必须 AOF + noeviction;Redis Cluster 配置必须启动拒绝。

Boundary

  • Redis 实现细节只存在于 qim-store,writer/mailbox 不拼 key、不写 Lua。
  • 复合 mailbox_seq 不以超出 53 位精度的 double 参与范围判断。
  • 邮箱按日桶保留 30 天;DispatchProgress 只在 7 天 replay-safe 窗口后 GC;客户端提交/去重窗口为 2 小时,历史由 retention_class 决定。

Forbidden Zone

  1. 禁止把完整 63 位 mailbox_seq 直接作为 Redis ZSET score/Lua number。
  2. 禁止每页把所有日桶全量拉回内存后再截断。
  3. 禁止 Redis 不可用时把必需集成测试标绿。
  4. 禁止只裁剪当天 key 或写入从未被读取的裁剪水位。
  5. 禁止以局部 hash tag、按槽 Lua 或普通 Redis 客户端伪造 Redis Cluster 支持;Cluster 端点必须拒绝启动。

2.5 Scylla MessageStore / MailboxStore

Capability

  • RF=3、关键写读为 LOCAL_QUORUM,使用预编译语句和绑定分区键。
  • u128 使用 16 字节大端 blob;u64 写入 CQL 前统一断言 bit63 为零。
  • message_record 表级 TTL 为 0,逐行保留策略;所有 MessageStore 表禁止显式 DELETE。
  • 双实例段租约、重启接管、并发裁剪均保持单调且不复用序号。

Boundary

  • Scylla 专属 schema/驱动代码只在 store/scylla 存储线。
  • 投影保留 Redis 的混合形态遵循 ADR-0009,不得在冲突合并中退回已停止写入的 Redis 历史。

Forbidden Zone

  1. 禁止用无条件 INSERT 初始化 CAS/LWT 保护的分配器或裁剪水位。
  2. 禁止把未实际发出的整段号永久登记为连续水位空洞。
  3. 禁止 RF=1 或驱动默认 ONE 冒充生产持久性验证。
  4. 禁止大范围 SELECT 使用 execute_unpaged。
  5. 禁止测试环境无 ScyllaDB 时静默跳过发布门禁。

2.6 SDK 与本地持久化

Capability

  • 默认磁盘库;应用重启后恢复消息、会话、游标、已读水位和 pending。
  • 登录同步必须先对账再重发 pending;实时推送不推进游标。
  • 历史、邮箱和实时消息保留 message_type / custom_type。
  • 本地会话预览来自本地已落库的最后消息,未读按唯一定义式现算。

Boundary

  • SDK 不负责产品 UI,不自动把“收到消息”解释成“用户已读”。
  • 聊天室只在内存态,不进入持久会话/未读。
  • 本地库路径与平台目录由宿主决定,但默认值必须稳定且按用户隔离。

Forbidden Zone

  1. 禁止 GUI/CLI 默认使用退出即丢的内存库。
  2. 禁止迁移失败只清消息、不清游标,或清掉 local_pending。
  3. 禁止用 conversation_seq 跨会话排序或用客户端时间作为同步下界。
  4. 禁止在 MessagesAdded 再累加一次已由 ConversationsUpdated 覆盖的未读。
  5. 禁止动态 import、any 强转和无需求的 try/catch。

2.7 GUI / CLI 产品层

Capability

  • 登录/重登、连接状态、会话列表、预览、历史分页、发送状态、单聊、群聊、建群、成员页、聊天室、错误/顶号/本地库重置状态均可操作与呈现。
  • 建群只创建新群;服务端绑定创建者,已存在群不可借 CREATE_GROUP 越权修改。
  • UI 对异步命令以事件终态为准,不在命令入队后伪造成功。

Boundary

  • 不实现服务端不存在的媒体、E2EE、离线推送等能力。
  • 不自行维护第二套协议、游标、去重或未读算法。

Forbidden Zone

  1. 禁止自动登录捕获旧表单值,或在用户确认前连接默认账号。
  2. 禁止把 SEND_ACK 显示成“对方已送达/已读”。
  3. 禁止 CREATE_GROUP、PULL_MEMBERS、PULL_HISTORY 绕过成员/创建者授权。
  4. 禁止命令仅成功入队就更新为“建群成功”。
  5. 禁止无正文引用被渲染为空白而不提供回源/占位说明。

2.8 分支同步与发布门禁

Capability

  • 共享协议、客户端、测试、CommitLog/Fanout 契约只在 main 开发。
  • store/redis 单向吸收 main;store/scylla 吸收 main 后只保留后端专属差异。
  • 三条线各自执行后端真实集成测试、功能 E2E 和性能测试。

Boundary

  • 不把 Scylla 专属代码反向覆盖 main 的 Redis 实现。
  • 不用一条存储线的绿色结果替另一条线背书。

Forbidden Zone

  1. 禁止在脏工作树中直接 checkout/merge 覆盖用户改动。
  2. 禁止把 store/scylla 的契约核心变更直接回流 main。
  3. 禁止发布门禁依赖“测试跳过”。
  4. 禁止只跑 30 秒冒烟就声明 180 秒或 4 小时容量结论。

3. Validation Plan

3.1 Must Have(发布阻断)

ID 输入/动作 必须观察到的结果
M-COMMIT-01 正常发送一条消息 ACK 前 MessageRecord 与 Outbox 已可从独立进程读到
M-COMMIT-02 在 dedup 占位后 kill -9 writer 重启/接管后沿同一坐标完成提交,不产生第二条消息
M-COMMIT-03 在 MessageRecord 后、Outbox 前 kill -9 恢复器发布 Outbox,最终投递;客户端重试回同一 ACK
M-COMMIT-04 Outbox 后、COMMITTED 前 kill -9 允许日志重复,最终逻辑消息仅一条
M-COMMIT-05 空/短/全零 cmid 返回协议错误,历史和邮箱均无新消息
M-LOG-01 Fanout 处理后在事务提交前失败 Outbox 位点不前进,恢复后重新处理
M-LOG-02 同一 Outbox 记录处理两次 dispatch_id、分区和内容完全相同
M-MB-01 mailbox 物化后、日志位点提交前崩溃 重放复用原 mailbox_seq,邮箱逻辑集合不重复
M-MB-02 某收件人 append 失败后恢复 水位先停后追平,所有收件人最终可见
M-MB-03 Redis mailbox 数据保留但本地/消费位点丢失 从 min(W)+1 重放,不复用旧 offset
M-REDIS-01 epoch=32 的相邻 offset 范围查询能区分并严格排序,不跳项
M-REDIS-02 跨日 created_at 与 mailbox_seq 逆序 分页仍按 mailbox_seq 严格递增,covered 不跨项
M-REDIS-03 损坏一条编码 请求失败且游标不推进,不静默过滤
M-REDIS-04 三节点 Redis Cluster 端点 writer / mailbox 非零退出、readiness 不绿、零提交/邮箱写入;该负向门禁失败即不得发布
M-SCYLLA-01 两实例同时初始化同一分配器 只产生互斥、不重叠的号段
M-SCYLLA-02 段内任意位置重启 未实际发出的号不永久卡住水位
M-SCYLLA-03 单副本停止(RF3) LOCAL_QUORUM 路径继续满足持久性,恢复后数据一致
M-HIST-01 TEXT/CUSTOM/MEDIA 历史回源 类型、custom_type、正文和 sender 完整往返
M-HIST-02 OLDER/NEWER、limit/max_bytes 方向和边界逐项生效;空洞不导致无限扫描
M-AUTH-01 非成员拉历史/成员页 明确拒绝,零数据泄漏
M-AUTH-02 非成员对已有 group_id 发 CREATE_GROUP 拒绝且成员集合不变
M-CLIENT-01 磁盘库写入后销毁并重开 会话、预览、历史、已读、游标、pending 全部恢复
M-CLIENT-02 ACK 丢失后关闭客户端并重开 先邮箱对账,命中后不重发;未命中才重发
M-CLIENT-03 GUI 默认启动 使用稳定用户库路径,不自动连接默认账号
M-CLIENT-04 建群请求 仅在 MembersUpdated/错误事件后呈现成功/失败
M-BRANCH-01 三分支同一 E2E 套件 main、store/redis、store/scylla 各自真实执行且通过
M-PERF-01 请求目标速率 实际到达率达到目标下限后才允许判 SLO;低速不能假绿

3.2 Need Have

  • Redis 日桶读取使用 pipeline/有界 LIMIT,内存与响应字节上界可观测。
  • HLC 在固定 writer_id、重启与时钟回拨下不重复。
  • 客户端契约向量由服务端与 SDK 双跑。
  • GUI 至少有一套自动化命令级测试;图形 E2E 覆盖重启恢复。
  • outbox lag、orphan record、dispatch retry、recovery count 均有 qim_ 指标。

3.3 Should Have

  • 100 次 writer/mailbox 随机 kill -9 混沌循环,消息集合差集为零。
  • Redis Cluster 三节点负向启动拒绝验证;不允许将 Cluster 标绿或作为已支持后端。
  • Scylla tombstone、Paxos、分片路由与查询行数指标纳入报告。
  • Windows/macOS/Linux GUI 打包冒烟。

4. Failure & Edge Cases

故障 预期行为
Redpanda 暂时不可用 不返回 ACK;commit intent 保留,恢复后继续;客户端 pending 不丢
Redis/Scylla MessageRecord 写失败 Outbox 不提交;请求失败或保持 pending,可幂等重试
Outbox 重复 Fanout/Mailbox 以稳定 ID 幂等吸收
分发日志保留窗口将耗尽 P1 告警并停止危险裁剪,不静默丢恢复源
MailboxStore 编码损坏 停止游标推进,返回可诊断错误
客户端库版本过新/迁移失败 触发 LocalStoreReset,保留 pending,游标一并重置
GUI 被顶号 终态、不自动重连;用户显式重新登录
创建已存在群 AlreadyExists/PermissionDenied,绝不追加成员
群超过一期上限 整批拒绝,不截断成员列表
历史正文已治理删除 body_included=false 明确占位,不当作空正文

5. Audit / Logs / Metrics

  • 日志必须携带同一 trace_id,但禁止正文、token、密钥、完整游标签名。
  • 至少新增/验证:
  • qim_commit_inflight_total
  • qim_commit_recovery_total
  • qim_outbox_publish_latency_us
  • qim_outbox_lag_us
  • qim_dispatch_replay_total
  • qim_message_orphan_total(恒零)
  • qim_dispatch_orphan_total(恒零)
  • qim_mailbox_decode_error_total(恒零)
  • 指标标签禁止包含 user_id、message_id、conversation_id。

6. Security / Token Constraints

  • 客户端身份只取 gateway 已认证上下文,禁止信任上行自报 sender/owner。
  • CREATE_GROUP 将创建者绑定为认证用户;已存在群不可通过该 opcode 修改。
  • PULL_HISTORY/PULL_MEMBERS/SEND_MESSAGE 都执行成员准入。
  • 内部管理 RPC 生产环境必须经 mTLS/网络隔离;客户端 opcode 不复用无鉴权管理语义。
  • TestPlan 的 destructive 测试只针对带唯一名称的隔离容器和专用端口。

7. Integration Tests

  1. Gateway → MessageCommitter:认证用户、cmid、消息类型完整传递;无效 cmid 在 gateway 拒绝。
  2. MessageCommitter → MessageStore:首次、恢复中、已提交三种状态在 Redis/Scylla 返回一致语义。
  3. MessageCommitter → Outbox:只有 MessageRecord 可恢复后才发布;发布成功才允许 COMMITTED。
  4. Outbox → Fanout:事务提交同时包含 dispatch 记录和消费位点。
  5. Dispatch log → MailboxStore:partition/offset 与 mailbox_seq 一一对应;重放同值覆盖。
  6. MailboxStore → SDK:PUSH 与 PULL 共用字段,类型/custom_type 保真;只有 batch 推进游标。
  7. SDK → GUI/CLI:事件先于命令返回、命令先于事件两种竞态都不产生幽灵 pending 或伪成功。
  8. main → store/redis/store/scylla:共享 proto、契约测试与客户端代码无漂移。

8. E2E User Flows

E2E-1 正常发送与历史持久化

  1. A/B 登录;验证 Online。
  2. A 发 TEXT 与 CUSTOM;验证 pending 出现。
  3. 验证 ACK 后 Outbox/MessageRecord 均存在。
  4. B 在线收取;验证类型与正文。
  5. 重启 writer、mailbox、gateway;B 拉历史仍完整。
  6. 错误路径:Redpanda 不可用时 A 不收到假 ACK;恢复后同 cmid 完成。

E2E-2 提交故障恢复

  1. 在四个提交故障点逐一注入 kill -9。
  2. 每次重启后用原 cmid 重试。
  3. 验证 ACK 坐标相同、历史一条、A/B 邮箱逻辑集合各一条。
  4. 错误路径:恢复超过阈值时告警,仍不得把 INFLIGHT 当 COMMITTED。

E2E-3 离线邮箱与 MailboxNode 接管

  1. B 离线,A 连续发送跨日期/跨分区构造消息。
  2. 在物化中重启 mailbox。
  3. B 登录分多页拉取;每页严格递增且无差集。
  4. 错误路径:注入损坏条目时游标停在其前,不跨越。

E2E-4 客户端重启恢复

  1. GUI 使用默认磁盘库登录并收发消息。
  2. 关闭应用,保留库文件;重新启动且不连接网络。
  3. 验证会话预览、本地历史、已读与 pending 可见。
  4. 恢复网络,验证先对账再重发且无重复。
  5. 错误路径:复制更高 schema 版本库,验证重置提示且 pending 保留。

E2E-5 群与授权

  1. A 创建新群并邀请 B;等待服务端成功事件后 UI 才显示群。
  2. A/B 拉成员、互发群消息、重启后拉历史。
  3. 非成员 C 拉成员/历史均被拒。
  4. C 使用同 group_id CREATE_GROUP 试图加入,必须失败且成员集合不变。

E2E-6 三存储线行为一致

  1. 在 main 与 store/redis 跑 E2E-1~5。
  2. 在 store/scylla 的 3 节点环境跑同一套流程。
  3. 对同输入比较客户端可见集合、排序、类型、错误码。
  4. 错误路径:任一后端测试被跳过即整条分支失败。

E2E-7 性能与稳定性

  1. 先验证宿主空闲与实际到达率下限。
  2. Redis、Scylla 分别跑 180 秒稳态和 T-HEAVY。
  3. 记录 ACK、端到端、outbox lag、物化、数据库指标。
  4. 错误路径:实际吞吐不足或恒零指标非零时不得引用 P99 结论。

9. E2E Coverage Matrix

Capability E2E Goals Covered?
MessageCommitter / ACK 语义 E2E-1、E2E-2 ✓
Outbox / Fanout 事务 E2E-1、E2E-2 ✓
Mailbox 重放 / 水位 E2E-2、E2E-3 ✓
Redis 后端 E2E-3、E2E-6、E2E-7 ✓
Scylla 后端 E2E-2、E2E-6、E2E-7 ✓
SDK 本地持久化与对账 E2E-1、E2E-4 ✓
GUI/CLI 完整交互 E2E-4、E2E-5 ✓
群授权 E2E-5 ✓
分支同步 E2E-6 ✓
性能与恒零指标 E2E-7 ✓

10. Environment Spec

字段 值
type local + Docker 隔离依赖
exec /app/q-im 本地 shell;依赖容器使用唯一 compose project name
workdir /app/q-im
ports 应使用测试专用 Redis/Redpanda/Scylla 端口,禁止复用未知的 6379 数据
env_vars QIM_REDIS_ADDR、QIM_REDPANDA_BROKERS、QIM_SCYLLA_ADDR、服务监听端口
auto_provisioned 后续实现阶段补齐 compose;当前 Docker daemon 可用

发布验证不得执行会清理共享 Redis 的脚本;FLUSHALL、容器删除和进程终止的目标必须先解析为本 TestPlan 创建的隔离实例。