ADR-0020:GroupDispatch.fencing_epoch 在一期形态不适用,切换到会话租约序号窗口时强制启用¶
- 状态:已接受(2026-09-26,由项目负责人选择方案 B)
- 相关:
docs/PLAN.md§19.1 边界 1、§19.2(fencing token 的载体与两个校验点)、ADR-0007(一期简化形态)、 ADR-0012(Fanout 不用 Kafka 事务)
背景¶
§19.2 规定:fencing_epoch 由 ShardRegistry 在每次授予会话写入租约时递增,ConversationWriter 把它写进
每条 GroupDispatch,MailboxNode 按 (tenant, conversation) 维护“已见最大 epoch”过滤器,丢弃旧 owner
的迟到追加。它防的是会话写入权脑裂:旧 writer 在租约失效后仍凭本地持有的序号预留窗口与
ConversationHead 写入资格继续提交,与新 writer 分配出重叠的 conversation_seq。
此前该字段被列为发布阻断。实现它需要 ShardRegistry(etcd)与会话写入租约,二者一期均未实现。
一期形态下,fencing 所防的脑裂不存在¶
- writer 不持有任何正确性状态:
conversation_seq分配、幂等(ClientDedup)与提交状态机都在 MessageStore 的 Redis Lua 中原子完成,按会话提交桶线性化;writer 没有本地序号窗口,任何 writer 处理 任何会话都正确(gateway 的会合哈希只是亲和性优化)。不存在“旧 owner 凭本地窗口继续发号”的路径。 - 暂停后恢复的 writer:它手中的在途提交已在 Redis 固化坐标,恢复后发布的 Outbox 记录与恢复器
可能发布的记录同值,下游以确定性
dispatch_id + payload_digest收敛,不产生新序号。 writer_id被他人接管:HLC 窗口按(region_id, writer_id)在 Redis 原子领取,新窗口起点 严格大于已登记的上一窗口终点,同一writer_id的两个进程拿到的毫秒区间互不重叠,message_id不会重复(identity::RESERVE_HLC_SCRIPT)。- 序号回退:宿主崩溃导致的 Redis 回退已由 ADR-0018 的
appendfsync always强制消除。
回归测试 qim-store/tests/redis_message_store.rs::两个_writer_并发提交同一会话_序号不重复且连续
以两个独立存储实例并发提交 200 条,断言序号恰为 1..=200。
决策¶
- 一期形态(Redis Lua 原子分配序号、无会话写入租约)不实现
GroupDispatch.fencing_epoch, 它不再是一期发布阻断项。§19.1 边界 1 的保证机制在一期由“Redis 原子分配 +appendfsync always” 承担,检测信号(同一(conversation_id, conversation_seq)出现两个不同message_id)由 ADR-0018 归档层的冲突拒绝与qimctl audit dispatch的一致性检查覆盖。 - 强制启用条件:一旦引入以下任一机制,
fencing_epoch与 §19.2 的两个校验点必须先于该机制上线: - ConversationWriter 在本地持有序号预留窗口(按段预取
conversation_seq); - 会话写入改由 ShardRegistry 租约授予(包括阶段一迁移到 ScyllaDB 分配器后的任何租约化形态);
ConversationHead改为由 writer 条件更新。- §19.2 的契约文本保持不变(目标形态);
docs/PLAN.md§0.2 记录本 ADR 的适用范围。
后果¶
- 一期少一个发布阻断项,不引入 ShardRegistry。
- 代价:writer 的线性化点完全依赖 MessageStore Redis 的原子性与持久性;该实例不可用即无法提交 (与 ADR-0007 的一期可用性取舍一致)。
- 任何把序号分配移出 Redis 原子脚本的改动,都必须先回到本 ADR 的“强制启用条件”。