fix: preserve snapshot version across epoch changes

This commit is contained in:
youfak 2026-08-02 10:43:42 +08:00
parent 6f5a2faea6
commit 596e4ab371
6 changed files with 23 additions and 9 deletions

View File

@ -102,6 +102,10 @@ Worker 自报的超前版本或 epoch 也必须拒绝。
和停止新分配的 `usable_until`
- 内容 `checksum`
`version` 在同一 Worker 流内严格连续递增,即使 `ownership_epoch` 因分配、Drain
或所有权恢复而前进也不重置。Gateway 只接受比当前 `version` 恰好大一的完整快照,
并拒绝倒退的 epoch因此 epoch 变化不会让仍在长连接内的刷新快照被误判为缺口。
`usable_until = expires_at - allocationSafetyMargin`。Worker 必须以
`usable_until` 作为最后可分配时刻;达到该时间后即使尚未到 `expires_at`
也不得再为新请求选择该 Proxy。

View File

@ -123,6 +123,9 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
Gateway 使用既有 `SessionSupervisor` 退避重连。Gateway 本地 Store 对从完整 Snapshot
消失且仍有 Active/Reserved 的 Proxy 已按 draining 继续上报,但 Controller 尚未具备
自动 `BeginDrain`/`AcknowledgeDrain` 编排或 Worker 分配器,不能视为自动 Drain 闭环。
- Snapshot 的 `version` 是同一 Worker 流的连续序列,`ownership_epoch` 是独立且只能前进
的权威栅栏epoch 变化不重置 version。此前 Gateway 错把 epoch 前进要求为 version=1
与 Controller 的 `lastAppliedVersion+1` 生成规则冲突,现已用连续版本规则统一。
## Git 同步事实2026-07-29

View File

@ -354,11 +354,11 @@ func TestAcquireSurvivesConcurrentSnapshotApply(t *testing.T) {
{epoch: 1, version: 2},
{epoch: 1, version: 3},
{epoch: 1, version: 4},
{epoch: 2, version: 1},
{epoch: 2, version: 2},
{epoch: 2, version: 3},
{epoch: 3, version: 1},
{epoch: 3, version: 2},
{epoch: 2, version: 5},
{epoch: 2, version: 6},
{epoch: 2, version: 7},
{epoch: 3, version: 8},
{epoch: 3, version: 9},
}
for _, step := range steps {
next := snapshot.Envelope{

View File

@ -267,9 +267,7 @@ func (s *Store) Apply(envelope Envelope) error {
switch {
case envelope.Epoch < current.Epoch:
return ErrResyncRequired
case envelope.Epoch == current.Epoch && envelope.Version != current.Version+1:
return ErrResyncRequired
case envelope.Epoch > current.Epoch && envelope.Version != 1:
case envelope.Version != current.Version+1:
return ErrResyncRequired
}
}

View File

@ -78,6 +78,12 @@ func TestStoreRejectsWrongWorkerVersionGapAndChecksum(t *testing.T) {
t.Fatalf("version gap error = %v, want ErrResyncRequired", err)
}
epochReset := base
epochReset.Epoch = 2
if err := store.Apply(epochReset); !errors.Is(err, ErrResyncRequired) {
t.Fatalf("epoch reset version error = %v, want ErrResyncRequired", err)
}
badChecksum := base
badChecksum.Version = 2
badChecksum.Checksum = "bad"
@ -272,7 +278,7 @@ func TestStoreApplyReusesCapacityAcrossVersionsAndEpochs(t *testing.T) {
third := second
third.Epoch = 2
third.Version = 1
third.Version = 3
third.Checksum = Checksum(third.Proxies)
if err := store.Apply(third); err != nil {
t.Fatalf("Apply(third): %v", err)

View File

@ -21,6 +21,9 @@
Delta 的 fail-closed 校验,也不把自动 Drain/ACK 记为已完成。
- Health Scheduler 现在先建立 EGRESS 与 Routing TARGET 的独立 due 引用,再投递
BASIC 任务;在单项小批次下,短 TTL Proxy 不会因 BASIC 先出队而错过首次出口/目标探测。
- 修复 Snapshot epoch/version 栅栏Worker 流的 `version` 在 ownership epoch 前进时
仍严格递增Gateway 以连续版本加非倒退 epoch 进行完整快照原子替换,避免动态分配后的
有效刷新被错误拒绝。这是后续自动 Drain 编排的必要前提。
## 2026-07-30