跨世界迁移:目标进程 Ready 不等于玩家可以 Commit

自动关联目录:跨世界迁移:目标进程 Ready 不等于玩家可以 Commit

核心判断:目标 DS 已启动、客户端已连上、玩家状态已准备和权威已切换是四个不同事实;只有带证据的 Commit 才能决定玩家属于哪个世界。

一、业务冲突

跨世界迁移同时涉及源 DS、目标 DS、客户端和宿主。任一方都只能观察部分事实:目标进程能监听,不代表场景和玩家已准备;客户端开始 Travel,不代表目标已接管;源端超时重试又可能与第一次请求同时到达 Commit,造成双重激活。

二、实际实现

实际迁移协调器把请求拆成 Prepare、目标分配、DS Prepared、Client Prepared、Player Prepared 和 Commit。阶段不能只靠枚举前跳,必须合并来自不同进程的证据;服务器自己的 Ready 无法伪造客户端条件。

每个请求带 Session Generation、RequestId 和 Attempt。每个玩家只允许一个在途迁移;Commit 幂等,重复提交返回原结果。只有无损失败可以重试,且复用原目标以避免同组玩家被重新匹配到不同实例;新 Attempt 会清空旧证据,迟到的旧 Commit 被拒绝。客户端开始恢复源场景后还会关闭提交窗口。

三、值得保留

这套设计抓住了分布式迁移的核心:阶段不是事实,证据才是事实。区分无损失败、已经重新进入源端和会话失败,也避免了“任何超时都自动再来一次”的危险策略。

四、结构性缺陷

协调状态主要在内存中,适合同进程或受控宿主,但进程崩溃后难以证明最后一次 Commit 是否已经生效。源端 Travel 后会主动忘记部分请求,目标端只能等待裁决;如果外部仲裁没有持久化 fencing token,严格 exactly-once 仍无法由客户端和两个 DS 自己保证。

五、目标设计

把迁移提升为可恢复的权威记录:

TransferRecord {
    SessionId session;
    RequestId request;
    Attempt attempt;
    SourceEpoch source;
    TargetEpoch target;
    EvidenceSet evidence;
    State state; // Prepared, Committed, Aborted
    StateDigest payload;
}

CommitResult Commit(RequestId id, Attempt a, Fence targetFence) {
    return store.CompareExchange(id, a, PlayerPrepared, Committed, targetFence);
}

源和目标只能根据持久化裁决激活玩家;StateDigest 证明传递的是同一份值状态,旧 epoch 的进程即使复活也没有提交权。

六、取舍与验证

持久仲裁增加延迟和外部依赖,局部试玩可继续使用内存实现,但协议字段和状态机应保持一致。故障测试要覆盖每个阶段前后进程退出、网络断开、重复消息、旧 Attempt 晚到、Commit 响应丢失、客户端已 Travel 后回源,以及一名玩家连续发起两个迁移。最终不变量只有一个:任意时刻最多一个世界拥有可提交的玩家权威。