diff --git a/docs/api/control-plane.md b/docs/api/control-plane.md index 0fb1b51..9dda089 100644 --- a/docs/api/control-plane.md +++ b/docs/api/control-plane.md @@ -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。 diff --git a/findings.md b/findings.md index f5c45b3..2f805b3 100644 --- a/findings.md +++ b/findings.md @@ -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) diff --git a/internal/gateway/dispatch/dispatcher_test.go b/internal/gateway/dispatch/dispatcher_test.go index 86c182e..5949c50 100644 --- a/internal/gateway/dispatch/dispatcher_test.go +++ b/internal/gateway/dispatch/dispatcher_test.go @@ -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{ diff --git a/internal/gateway/snapshot/store.go b/internal/gateway/snapshot/store.go index 9032e72..14f7a69 100644 --- a/internal/gateway/snapshot/store.go +++ b/internal/gateway/snapshot/store.go @@ -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 } } diff --git a/internal/gateway/snapshot/store_test.go b/internal/gateway/snapshot/store_test.go index 01dad2f..be1638a 100644 --- a/internal/gateway/snapshot/store_test.go +++ b/internal/gateway/snapshot/store_test.go @@ -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) diff --git a/progress.md b/progress.md index c63cfeb..8c9a785 100644 --- a/progress.md +++ b/progress.md @@ -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