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. 完成定义¶
本轮不是以“测试能过”作为完成,而是同时满足以下可观测结果:
SEND_ACK只在MessageRecord与可靠 Outbox 均已提交后返回。- writer、FanoutCoordinator、MailboxNode、Redis、ScyllaDB 或客户端在任一声明的故障点重启后,已确认消息最终仍能出现在历史和全部目标用户邮箱中。
- 同一
client_message_id、Outbox 事件或分发事件被重复处理时,只产生一条逻辑消息;允许至少一次传输,不允许重复气泡或新分配邮箱坐标。 main与store/redis的 Redis 后端、store/scylla的 ScyllaDB 后端通过同一份存储契约和 E2E 用户流程。- GUI 与 CLI 默认使用稳定的磁盘本地库;应用退出重启后,会话、历史、已读水位与 pending 可恢复。
- 客户端覆盖服务端一期已经实现的公开能力;服务端未实现的媒体、E2EE、离线推送等能力明确呈现为“不支持”,不伪造成功。
- 功能门禁、故障恢复门禁和性能门禁均在隔离环境中执行,不能通过“依赖不可用即跳过”得到绿色结果。
2. 子模块能力与边界¶
2.1 MessageCommitter(ConversationWriter 提交边界)¶
Capability
- 输入:已完成认证、成员校验、审核与限流的发送请求。
- 输出:
CommittedMessage或明确错误。 - 保证:幂等占位、坐标固化、MessageRecord 写入、Outbox 发布、状态转
COMMITTED的顺序可恢复。 SEND_ACK只能由成功的CommittedMessage生成。
Boundary
- 负责正文与 Outbox 的提交,不负责逐用户邮箱物化、在线推送和 UI 状态。
- 不在调用方暴露 Redis key、CQL 表、Kafka offset 等后端细节。
- 群成员快照/版本作为 Outbox 的确定性输入保存,恢复时不得重新猜测。
Forbidden Zone
- 禁止在 MessageRecord 或 Outbox 提交前发送
SEND_ACK。 - 禁止把 dedup 首次占位直接写成
COMMITTED。 - 禁止重复请求只回 ACK 而不恢复未完成的提交步骤。
- 禁止接受空、非 16 字节或全零
client_message_id并降级为非幂等发送。 - 禁止用客户端时间或随机数生成任何服务端排序/恢复坐标。
2.2 CommitLog 与 FanoutCoordinator¶
Capability
- Outbox 使用 Redpanda/Kafka API 的持久 topic。
- FanoutCoordinator 以
read_committed消费 Outbox,按目标 MailboxShard 生成稳定dispatch_id的分发事件。 - 产出分发事件与提交 Outbox 消费位点在同一事务中完成。
Boundary
- 不写用户邮箱,不直接写客户端 Socket。
- 不重新解释正文,不修改 MessageRecord。
- 分区键和 dispatch 拆分只由稳定路由规则决定。
Forbidden Zone
- 禁止把进程内 mpsc/TCP 写成功视为可靠 Outbox。
- 禁止自动消费者组 rebalance 改变 MailboxShard 的静态归属。
- 禁止在分发事件可靠提交前提交 Outbox 消费位点。
- 禁止使用随机
dispatch_id或重试时重新计算不同的成员集合。 - 禁止将 producer/consumer 的后端异常原样暴露成客户端协议细节。
2.3 Mailbox 分发日志消费与物化¶
Capability
- 从分发日志记录的 partition/offset 构造稳定
mailbox_seq。 - 同一 dispatch 重放复用同一
mailbox_seq、event_id和created_at。 - 全部收件人可靠落盘后推进连续水位;失败时不越过空洞。
- 接管起点取各 lane 水位最小值之后,而不是消费者组已提交 offset。
Boundary
- 负责邮箱引用、连续水位、投影事件与在线推送。
- 正文内联判定只执行 writer/Outbox 已给出的决定,不自行重算。
- 在线推送允许丢,个人邮箱不允许丢。
Forbidden Zone
- 禁止重放 dispatch 时重新分配新的 mailbox offset。
- 禁止物化失败后销账或推进水位。
- 禁止依赖只含 offset、没有原始分发事件的本地 pending 集合恢复。
- 禁止按日期桶拼接后直接推进
covered_through_seq。 - 禁止吞掉条目解码错误并让客户端游标跨过损坏记录。
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
- 禁止把完整 63 位
mailbox_seq直接作为 Redis ZSET score/Lua number。 - 禁止每页把所有日桶全量拉回内存后再截断。
- 禁止 Redis 不可用时把必需集成测试标绿。
- 禁止只裁剪当天 key 或写入从未被读取的裁剪水位。
- 禁止以局部 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
- 禁止用无条件 INSERT 初始化 CAS/LWT 保护的分配器或裁剪水位。
- 禁止把未实际发出的整段号永久登记为连续水位空洞。
- 禁止 RF=1 或驱动默认
ONE冒充生产持久性验证。 - 禁止大范围 SELECT 使用
execute_unpaged。 - 禁止测试环境无 ScyllaDB 时静默跳过发布门禁。
2.6 SDK 与本地持久化¶
Capability
- 默认磁盘库;应用重启后恢复消息、会话、游标、已读水位和 pending。
- 登录同步必须先对账再重发 pending;实时推送不推进游标。
- 历史、邮箱和实时消息保留
message_type/custom_type。 - 本地会话预览来自本地已落库的最后消息,未读按唯一定义式现算。
Boundary
- SDK 不负责产品 UI,不自动把“收到消息”解释成“用户已读”。
- 聊天室只在内存态,不进入持久会话/未读。
- 本地库路径与平台目录由宿主决定,但默认值必须稳定且按用户隔离。
Forbidden Zone
- 禁止 GUI/CLI 默认使用退出即丢的内存库。
- 禁止迁移失败只清消息、不清游标,或清掉
local_pending。 - 禁止用
conversation_seq跨会话排序或用客户端时间作为同步下界。 - 禁止在
MessagesAdded再累加一次已由ConversationsUpdated覆盖的未读。 - 禁止动态 import、
any强转和无需求的 try/catch。
2.7 GUI / CLI 产品层¶
Capability
- 登录/重登、连接状态、会话列表、预览、历史分页、发送状态、单聊、群聊、建群、成员页、聊天室、错误/顶号/本地库重置状态均可操作与呈现。
- 建群只创建新群;服务端绑定创建者,已存在群不可借 CREATE_GROUP 越权修改。
- UI 对异步命令以事件终态为准,不在命令入队后伪造成功。
Boundary
- 不实现服务端不存在的媒体、E2EE、离线推送等能力。
- 不自行维护第二套协议、游标、去重或未读算法。
Forbidden Zone
- 禁止自动登录捕获旧表单值,或在用户确认前连接默认账号。
- 禁止把
SEND_ACK显示成“对方已送达/已读”。 - 禁止 CREATE_GROUP、PULL_MEMBERS、PULL_HISTORY 绕过成员/创建者授权。
- 禁止命令仅成功入队就更新为“建群成功”。
- 禁止无正文引用被渲染为空白而不提供回源/占位说明。
2.8 分支同步与发布门禁¶
Capability
- 共享协议、客户端、测试、CommitLog/Fanout 契约只在
main开发。 store/redis单向吸收 main;store/scylla吸收 main 后只保留后端专属差异。- 三条线各自执行后端真实集成测试、功能 E2E 和性能测试。
Boundary
- 不把 Scylla 专属代码反向覆盖 main 的 Redis 实现。
- 不用一条存储线的绿色结果替另一条线背书。
Forbidden Zone
- 禁止在脏工作树中直接 checkout/merge 覆盖用户改动。
- 禁止把
store/scylla的契约核心变更直接回流 main。 - 禁止发布门禁依赖“测试跳过”。
- 禁止只跑 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_totalqim_commit_recovery_totalqim_outbox_publish_latency_usqim_outbox_lag_usqim_dispatch_replay_totalqim_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¶
- Gateway → MessageCommitter:认证用户、cmid、消息类型完整传递;无效 cmid 在 gateway 拒绝。
- MessageCommitter → MessageStore:首次、恢复中、已提交三种状态在 Redis/Scylla 返回一致语义。
- MessageCommitter → Outbox:只有 MessageRecord 可恢复后才发布;发布成功才允许 COMMITTED。
- Outbox → Fanout:事务提交同时包含 dispatch 记录和消费位点。
- Dispatch log → MailboxStore:partition/offset 与 mailbox_seq 一一对应;重放同值覆盖。
- MailboxStore → SDK:PUSH 与 PULL 共用字段,类型/custom_type 保真;只有 batch 推进游标。
- SDK → GUI/CLI:事件先于命令返回、命令先于事件两种竞态都不产生幽灵 pending 或伪成功。
- main → store/redis/store/scylla:共享 proto、契约测试与客户端代码无漂移。
8. E2E User Flows¶
E2E-1 正常发送与历史持久化¶
- A/B 登录;验证 Online。
- A 发 TEXT 与 CUSTOM;验证 pending 出现。
- 验证 ACK 后 Outbox/MessageRecord 均存在。
- B 在线收取;验证类型与正文。
- 重启 writer、mailbox、gateway;B 拉历史仍完整。
- 错误路径:Redpanda 不可用时 A 不收到假 ACK;恢复后同 cmid 完成。
E2E-2 提交故障恢复¶
- 在四个提交故障点逐一注入 kill -9。
- 每次重启后用原 cmid 重试。
- 验证 ACK 坐标相同、历史一条、A/B 邮箱逻辑集合各一条。
- 错误路径:恢复超过阈值时告警,仍不得把 INFLIGHT 当 COMMITTED。
E2E-3 离线邮箱与 MailboxNode 接管¶
- B 离线,A 连续发送跨日期/跨分区构造消息。
- 在物化中重启 mailbox。
- B 登录分多页拉取;每页严格递增且无差集。
- 错误路径:注入损坏条目时游标停在其前,不跨越。
E2E-4 客户端重启恢复¶
- GUI 使用默认磁盘库登录并收发消息。
- 关闭应用,保留库文件;重新启动且不连接网络。
- 验证会话预览、本地历史、已读与 pending 可见。
- 恢复网络,验证先对账再重发且无重复。
- 错误路径:复制更高 schema 版本库,验证重置提示且 pending 保留。
E2E-5 群与授权¶
- A 创建新群并邀请 B;等待服务端成功事件后 UI 才显示群。
- A/B 拉成员、互发群消息、重启后拉历史。
- 非成员 C 拉成员/历史均被拒。
- C 使用同 group_id CREATE_GROUP 试图加入,必须失败且成员集合不变。
E2E-6 三存储线行为一致¶
- 在 main 与 store/redis 跑 E2E-1~5。
- 在 store/scylla 的 3 节点环境跑同一套流程。
- 对同输入比较客户端可见集合、排序、类型、错误码。
- 错误路径:任一后端测试被跳过即整条分支失败。
E2E-7 性能与稳定性¶
- 先验证宿主空闲与实际到达率下限。
- Redis、Scylla 分别跑 180 秒稳态和 T-HEAVY。
- 记录 ACK、端到端、outbox lag、物化、数据库指标。
- 错误路径:实际吞吐不足或恒零指标非零时不得引用 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 创建的隔离实例。