跳转至

qsession canonical 历史投影影响分析

1. 变更摘要

qim-session 当前从已废弃且无人写入的 hist:{conversation_id} 读取会话头与未读数;新的可靠提交链路只维护 msgrecord:{commit}:... 与 msghistory:{commit}:...。因此消息已经持久化、推送和历史回放成功时,会话列表仍永久返回 latest_conversation_seq = 0、unread_count = 0。

本次变更让 qim-session 通过 qim-store::RedisMessageStore 的批量摘要接口读取 canonical MessageRecord/history,不恢复旧键,也不在 writer 热路径增加第二份历史写入。

2. 受影响模块与文件

模块 文件 影响
qstore 抽象 crates/qim-store/src/message/mod.rs 新增后端隐藏的 ConversationSummary 值对象
qstore Redis crates/qim-store/src/message/redis.rs 新增批量读取会话头和未读摘要的深接口;统一校验 canonical 索引与记录
qstore 导出 crates/qim-store/src/lib.rs 导出摘要值对象
qsession 上下文 crates/qim-session/src/ctx.rs 复用现有 Redis 连接构造 RedisMessageStore
qsession 未读 crates/qim-session/src/unread.rs 删除旧 hist:{cid} 解码,改用摘要接口映射 RPC
qsession 配置/说明 crates/qim-session/src/config.rs、main.rs 删除已失效的旧历史键契约与注释
协议常量 crates/qim-proto/src/consts.rs 增加精确未读扫描上限 200,避免散落魔数
集成测试 crates/qim-store/tests/redis_message_store.rs 覆盖收件人未读、本人排除、水位、损坏与扫描上限

3. 接口设计

Redis 专用深接口:

pub async fn conversation_summaries(
    &self,
    tenant_id: TenantId,
    user_id: UserId,
    states: &[(ConversationId, ConversationSeq)],
) -> Result<Vec<ConversationSummary>, MessageStoreError>;

states 保持调用方页面顺序;结果一一对应。摘要包含:

  • conversation_id
  • canonical latest_conversation_seq
  • canonical last_activity_id
  • canonical last_message_id
  • unread_count
  • unread_exact

Redis 键格式、8 字节大端序号 member、hash field 和 MessageRecord codec 均留在 qstore 内部。qsession 只处理领域值,不能直接依赖持久编码。

4. 读取算法与一致性

每页使用两轮 pipeline:

  1. 对每个会话读取最新 history member,并用 ZRANGEBYLEX 从已读水位之后最多取 200 个现存序号;序号允许空洞,禁止用 latest - read。
  2. 批量 HGET 精确窗口内的 canonical MessageRecord;窗口超过上限时只读取会话头。所有实际需要的记录都解码并验证 tenant、conversation 和 sequence;缺失、损坏或索引/记录不一致都返回 CorruptRecord,不能静默降级成零未读。

精确窗口内,未读只统计 sender.user_id != user_id、状态为 Normal|Edited 且会产生用户可见未读的消息;MessageType::Control、Recalled 与 Deleted 均排除,其余已知或未知业务类型计入。latest_seq - read_seq 超过 200 时返回饱和值 200 且 unread_exact = false,避免会话列表请求无界读取历史;这是序号距离上限,合法空洞也会触发保守近似。当前 RPC 尚无 unread_exact 字段,服务端先保留该事实,公开协议补充列为后续兼容升级项。

会话头必须来自最新 canonical MessageRecord,不能假设 last_activity_id == message_id。

5. 集成与兼容性

  • 会话集合与排序仍由 convs:{user} 投影维护;已读水位仍使用 read:{user}。
  • 消息头与未读事实改读 MessageStore canonical history。
  • 当前内部 Session RPC 未携带 tenant,现有阶段一服务也统一使用 legacy TenantId(0);本次显式保留该限制,不把它伪装成完整多租户支持。
  • 不迁移、不读取、不写入 hist:{cid};旧键可由运维在确认无旧版本服务后单独清理。

6. 风险与控制

风险 控制
每页 Redis 命令数随会话数增长 两轮 pipeline;页面仍有固定上限
大量未读导致读取放大 每会话最多精确扫描 200 条
canonical 索引残缺被误报为零 任一缺失/损坏显式失败
本人消息被计入未读 解码记录后按 sender 过滤
Control 或非活跃消息状态产生角标 排除 Control、Recalled 与 Deleted
tenant 缺失造成跨租户歧义 暂锁定 legacy tenant 0,并记录协议升级 Todo

复杂度评估:M。修改跨 qstore/qsession,但不改变公网协议或写入状态机。

7. 验证计划

  1. qstore Redis 集成:收件人看到 canonical head 和非零未读。
  2. qstore Redis 集成:发送者自己的消息不计未读。
  3. qstore Redis 集成:read_conversation_seq 之前的消息不计入。
  4. qstore Redis 集成:history member 对应记录缺失/损坏时返回 CorruptRecord。
  5. qstore Redis 集成:超过 200 条时 unread_count = 200 且 unread_exact = false。
  6. qsession 单测、check、clippy。
  7. 安全隔离的完整验收:从会话未读项继续跑完全部 15 项。