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_countunread_exact
Redis 键格式、8 字节大端序号 member、hash field 和 MessageRecord codec 均留在 qstore 内部。qsession 只处理领域值,不能直接依赖持久编码。
4. 读取算法与一致性¶
每页使用两轮 pipeline:
- 对每个会话读取最新 history member,并用
ZRANGEBYLEX从已读水位之后最多取 200 个现存序号;序号允许空洞,禁止用latest - read。 - 批量
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. 验证计划¶
- qstore Redis 集成:收件人看到 canonical head 和非零未读。
- qstore Redis 集成:发送者自己的消息不计未读。
- qstore Redis 集成:
read_conversation_seq之前的消息不计入。 - qstore Redis 集成:history member 对应记录缺失/损坏时返回
CorruptRecord。 - qstore Redis 集成:超过 200 条时
unread_count = 200且unread_exact = false。 - qsession 单测、check、clippy。
- 安全隔离的完整验收:从会话未读项继续跑完全部 15 项。