Mailbox 拉取游标错误映射诊断¶
Bug 摘要¶
个人邮箱已经在存储层识别裁剪游标,但 MailboxNode 没有把该结果编码为下行协议
ERROR{code=CURSOR_EXPIRED}。请求任务仅记录警告后结束,Gateway 与客户端都收不到
响应,因此 SDK 无法进入既有的 REBUILD 流程。处于启动回放或 MailboxDirty 状态时也
存在同一类缺陷:虽然发出了 ERROR,但错误码仍是 protobuf 默认值 UNKNOWN,且发送
失败被静默忽略。
已验证假设¶
- 假设:Redis
RangeScan对早于裁剪水位的游标有可区分的错误。 - 证据:
qim-store/src/redis/store.rs在after_seq < user_trim_seq时返回StoreError::CursorExpired { trim_watermark }。 - 结果:确认。
- 假设:该错误在 MailboxNode 被转成协议响应。
- 证据:
qim-mailbox/src/pull.rs对range_scan(...).await使用?;调用方qim-mailbox/src/net.rs只记录“邮箱拉取失败”。两处均没有构造RpcKind::Error。 - 结果:否定。
- 假设:客户端已有消费
CURSOR_EXPIRED的恢复路径。 - 证据:
qim-sdk/src/state.rs对ErrorCode::CursorExpired进入Rebuilding并发送PullSessionList;qim-sdk/src/wire.rs将数值码1映射为该错误。 - 结果:确认;缺口仅在 MailboxNode 的协议边界。
- 假设:未 ready/dirty 已使用协议定义的错误码。
- 证据:
qim-mailbox/src/pull.rs构造RpcKind::Error时未设置error_code,默认值为0;而qim-proto/proto/qim/common.proto已定义ERROR_CODE_MAILBOX_DIRTY = 4。 - 结果:否定。
根因¶
- 直接原因:拉取处理器把
StoreError作为内部anyhow::Result向上传播,而网络层把 它当作仅供日志记录的任务错误,未产生请求级响应。 - 根本原因:存储错误到稳定协议错误码的适配缺失;同一处的错误帧使用
.ok(),将断开 或拥塞导致的响应发送失败吞掉,违反错误响应与请求同等可靠的边界契约。
最小修复¶
在 pull.rs 集中构造并可靠发送错误帧:
StoreError::CursorExpired映射为协议常量ErrorCode::CursorExpired(数值1);- 非法客户端 mailbox 序列映射为
ErrorCode::CursorInvalid(数值3); - 未 ready、dirty、Redis 后端不可用或存储状态不安全映射为
ErrorCode::MailboxDirty(数值4),并给客户端稳定、非后端实现细节的说明; - 发送
ERROR使用send(...).await并向连接任务返回断开错误,绝不使用.ok(); - 上述错误路径返回前不发送
MailboxBatch,因此不会推进客户端游标。
新增回归测试验证裁剪错误、未 ready/dirty 错误及后端错误各自产生唯一的协议错误帧,且 没有成功批次。SDK 对 code 4 的具体重试策略由并行负责方处理,不在本修复范围内。
影响文件¶
crates/qim-mailbox/src/pull.rscrates/qim-mailbox/src/net.rs(保留其仅处理连接级发送失败的职责)docs/designs/diagnosis_20260823_mailbox_cursor_error_mapping.md
回归风险¶
低。成功拉取路径与存储语义不变;变更仅将此前无响应或码为 UNKNOWN 的失败显式化。
客户端会首次接收到正确错误码,因而必须确认 SDK 的 CURSOR_EXPIRED 重建链路及并行中的
MAILBOX_DIRTY 处理均通过。内部响应连接已关闭时仍会结束该请求任务,这是唯一无法向已
断开客户端交付响应的情形。