fix: align sequential routing defaults

This commit is contained in:
youfak 2026-07-29 21:41:17 +08:00
parent 35a201cab1
commit 2dbfe2f742
10 changed files with 78 additions and 19 deletions

View File

@ -192,7 +192,7 @@ routing:
strategy: strategy:
type: sequential type: sequential
switchAfterEmptyFetch: 5 switchAfterEmptyFetch: 5
endBehavior: stayLast endBehavior: stop
onUnavailable: onUnavailable:
action: reject action: reject
waitTimeout: 0s waitTimeout: 0s
@ -203,7 +203,9 @@ routing:
- `strategy.type` 支持 `sequential`、`random`、`roundRobin`、`weighted`、 - `strategy.type` 支持 `sequential`、`random`、`roundRobin`、`weighted`、
`leastConnections` `leastConnections`
- `weighted` 使用 `weights` 映射,键必须引用本 Routing 的 Upstream。 - `weighted` 使用 `weights` 映射,键必须引用本 Routing 的 Upstream。
- `sequential` 必须设置大于零的 `switchAfterEmptyFetch` - `sequential` 至少引用两个 Upstream并设置大于零的
`switchAfterEmptyFetch``endBehavior` 省略时默认为 `stop`,也可显式设置
`loop``stayLast`
- `onUnavailable.action``reject`、`wait` 或 `direct`;默认建议 `reject` - `onUnavailable.action``reject`、`wait` 或 `direct`;默认建议 `reject`
Sequential 的空计数属于 Upstream当前索引属于 Routing。只有 Provider 响应 Sequential 的空计数属于 Upstream当前索引属于 Routing。只有 Provider 响应
@ -350,7 +352,7 @@ PostgreSQL 故障本身不应使 Redis 中可完成的 Extract 返回 `503`。Me
2. 所有启用监听器具有合法 `host:port` 2. 所有启用监听器具有合法 `host:port`
3. 非回环监听器满足认证或来源 CIDR 保护。 3. 非回环监听器满足认证或来源 CIDR 保护。
4. Routing 名称唯一,正则可编译,引用的 Upstream 存在。 4. Routing 名称唯一,正则可编译,引用的 Upstream 存在。
5. Sequential 阈值大于零,`onUnavailable.action` 明确。 5. Sequential 至少引用两个 Upstream、阈值大于零,`onUnavailable.action` 明确。
6. 启用的 Upstream 有正数 `pool.maxSize`、并发和 Fetch 限制。 6. 启用的 Upstream 有正数 `pool.maxSize`、并发和 Fetch 限制。
7. `allocationSafetyMargin < ttl` 7. `allocationSafetyMargin < ttl`
8. `fetch.maxTotal == 0``fetch.maxTotal >= pool.maxSize` 8. `fetch.maxTotal == 0``fetch.maxTotal >= pool.maxSize`

View File

@ -272,6 +272,9 @@ sequenceDiagram
Upstream 的 Empty 事实全局共享;每条 Routing 独立 CAS 当前索引。多个并发 Upstream 的 Empty 事实全局共享;每条 Routing 独立 CAS 当前索引。多个并发
协程只能有一个成功从 A 切到 B其他协程读取新版本不会再切到 C。 协程只能有一个成功从 A 切到 B其他协程读取新版本不会再切到 C。
Sequential 至少配置两个 Upstream列表耗尽后的默认行为是 `stop``loop` 和
`stayLast` 必须显式配置。disabled Upstream 不参与新分配,其运行时跳过与权威
游标持久化仍由后续 Routing Runtime 完成。
## 10. Exclusive Extraction ## 10. Exclusive Extraction

View File

@ -58,6 +58,9 @@ Gateway Worker 只读取本地不可变快照并维护本地容量计数。供
### 4.3 Sequential 切换 ### 4.3 Sequential 切换
Sequential 至少配置两个 Upstream初始使用列表第一项。列表耗尽时默认 `stop`
也可显式选择 `loop``stayLast`
1. Provider 响应成功且解析成功,但合法候选为零,才累计 Empty。 1. Provider 响应成功且解析成功,但合法候选为零,才累计 Empty。
2. 网络、认证、HTTP、模板或解析失败只计 Error。 2. 网络、认证、HTTP、模板或解析失败只计 Error。
3. 全部候选均重复时计 DuplicateOnly并重置连续 Empty。 3. 全部候选均重复时计 DuplicateOnly并重置连续 Empty。

View File

@ -99,6 +99,10 @@ test/{fixtures,integration,e2e,load}/
- [ ] Cover four-empty-then-success, five-empty, A-to-B-only, disabled references, - [ ] Cover four-empty-then-success, five-empty, A-to-B-only, disabled references,
end behavior, and explicit onUnavailable. end behavior, and explicit onUnavailable.
当前进度2026-07-29领域构造器与严格配置已统一 Sequential 至少两个
Upstream、`endBehavior` 默认 `stop`并覆盖列表末端停止disabled candidate、
跨实例恢复和 `onUnavailable` 运行链仍待完成。
## Task 5: Provider Fetch Classification and Scheduling ## Task 5: Provider Fetch Classification and Scheduling
**Files:** `internal/controller/provider/*.go`, `internal/domain/upstream/*.go`, tests **Files:** `internal/controller/provider/*.go`, `internal/domain/upstream/*.go`, tests

View File

@ -115,8 +115,8 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
503wait/direct 和默认 reject 尚未形成运行时闭环。 503wait/direct 和默认 reject 尚未形成运行时闭环。
- Sequential 当前版本 CAS 只在单进程内生效,构造时总从第一个 Upstream 开始; - Sequential 当前版本 CAS 只在单进程内生效,构造时总从第一个 Upstream 开始;
PostgreSQL 管理态尚未接入游标恢复和跨实例 CAS。 PostgreSQL 管理态尚未接入游标恢复和跨实例 CAS。
- 单 Upstream Sequential、默认 endBehavior、disabled candidate 的最终语义与 - 对话最终语义已确认并落实:单 Upstream Sequential 启动校验失败,
现有代码/需求存在差异,进入设计确认项,不在测试中固化未确认行为 `endBehavior` 省略时默认 `stop`disabled candidate 的运行时推进仍待实现
## Proxy Capacity 验收审计2026-07-29 ## Proxy Capacity 验收审计2026-07-29
@ -131,8 +131,8 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
## Routing Runtime 设计输入2026-07-29 ## Routing Runtime 设计输入2026-07-29
- `对话内容.md` 最后一个明确建议将 Sequential `endBehavior` 默认设为 `stop` - `对话内容.md` 最后一个明确结论将 Sequential `endBehavior` 默认设为 `stop`
当前领域构造器默认 `stayLast`,配置参考只展示显式值,三者需要统一 领域构造器、配置校验与配置参考现已统一,且拒绝单 Upstream Sequential
- 产品安全默认已确定为 `onUnavailable=reject`,但当前严格配置要求字段必填; - 产品安全默认已确定为 `onUnavailable=reject`,但当前严格配置要求字段必填;
需要确认省略时自动补 reject还是继续拒绝启动。 需要确认省略时自动补 reject还是继续拒绝启动。
- disabled Upstream 的确定语义是停止 Fetch、健康检查和新分配已有 Proxy/连接 - disabled Upstream 的确定语义是停止 Fetch、健康检查和新分配已有 Proxy/连接

View File

@ -35,7 +35,7 @@ routing:
- name: extract - name: extract
enabled: true enabled: true
purpose: extract purpose: extract
upstreams: [provider-a] upstreams: [provider-a, provider-b]
strategy: strategy:
type: sequential type: sequential
switchAfterEmptyFetch: 5 switchAfterEmptyFetch: 5
@ -43,7 +43,7 @@ routing:
onUnavailable: onUnavailable:
action: reject action: reject
upstreams: upstreams:
provider-a: provider-a: &valid-upstream
enabled: true enabled: true
exposure: [gateway, extract] exposure: [gateway, extract]
provider: provider:
@ -78,6 +78,7 @@ upstreams:
maxAttempts: 2 maxAttempts: 2
maxConsecutiveFailures: 3 maxConsecutiveFailures: 3
urls: [http://connect.rom.miui.com/generate_204] urls: [http://connect.rom.miui.com/generate_204]
provider-b: *valid-upstream
` `
func TestLoadStrictValidConfiguration(t *testing.T) { func TestLoadStrictValidConfiguration(t *testing.T) {
@ -133,7 +134,7 @@ func TestValidateRejectsUnprotectedPublicListener(t *testing.T) {
} }
func TestValidateRejectsMissingUpstreamReference(t *testing.T) { func TestValidateRejectsMissingUpstreamReference(t *testing.T) {
broken := strings.Replace(validConfig, "upstreams: [provider-a]", "upstreams: [missing]", 1) broken := strings.Replace(validConfig, "upstreams: [provider-a, provider-b]", "upstreams: [provider-a, missing]", 1)
_, err := Load(strings.NewReader(broken)) _, err := Load(strings.NewReader(broken))
if err == nil || !strings.Contains(err.Error(), "missing") { if err == nil || !strings.Contains(err.Error(), "missing") {
t.Fatalf("Load() error = %v, want missing upstream error", err) t.Fatalf("Load() error = %v, want missing upstream error", err)
@ -443,6 +444,13 @@ func TestValidateRejectsInvalidConfigurationMatrix(t *testing.T) {
}, },
want: "endBehavior", want: "endBehavior",
}, },
{
name: "sequential requires two upstreams",
mutate: func(cfg *Config) {
cfg.Routing[0].Upstreams = []string{"provider-a"}
},
want: "at least two upstreams",
},
{ {
name: "weighted strategy missing weight", name: "weighted strategy missing weight",
mutate: func(cfg *Config) { mutate: func(cfg *Config) {
@ -456,9 +464,10 @@ func TestValidateRejectsInvalidConfigurationMatrix(t *testing.T) {
cfg.Routing[0].Strategy = Strategy{Type: "weighted", Weights: map[string]int{ cfg.Routing[0].Strategy = Strategy{Type: "weighted", Weights: map[string]int{
"provider-a": 1, "provider-a": 1,
"provider-b": 1, "provider-b": 1,
"provider-c": 1,
}} }}
}, },
want: "provider-b", want: "provider-c",
}, },
{ {
name: "weighted strategy nonpositive weight", name: "weighted strategy nonpositive weight",

View File

@ -187,6 +187,9 @@ func validateStrategy(scope string, upstreams []string, strategy Strategy) error
return err return err
} }
if strategy.Type == "sequential" { if strategy.Type == "sequential" {
if len(upstreams) < 2 {
return fmt.Errorf("validate %s strategy: sequential requires at least two upstreams", scope)
}
if err := requirePositive(scope+" strategy.switchAfterEmptyFetch", strategy.SwitchAfterEmptyFetch); err != nil { if err := requirePositive(scope+" strategy.switchAfterEmptyFetch", strategy.SwitchAfterEmptyFetch); err != nil {
return err return err
} }

View File

@ -96,6 +96,33 @@ func TestSequentialValidFetchResetsEmptyCount(t *testing.T) {
} }
} }
func TestSequentialDefaultsToStopAfterLastUpstream(t *testing.T) {
sequence, err := NewSequential([]string{"a", "b"}, 1)
if err != nil {
t.Fatalf("NewSequential(): %v", err)
}
if !sequence.ObserveEmpty("a") || sequence.Current() != "b" {
t.Fatalf("first transition current = %q, want b", sequence.Current())
}
if !sequence.ObserveEmpty("b") {
t.Fatal("ObserveEmpty(b) = false, want transition to stopped")
}
if current, available, _ := sequence.CurrentSelection(); available || current != "" {
t.Fatalf("CurrentSelection() = %q, %v; want stopped", current, available)
}
}
func TestSequentialRejectsSingleUpstream(t *testing.T) {
if _, err := NewSequential([]string{"a"}, 1); err == nil {
t.Fatal("NewSequential(single upstream) error = nil")
}
if _, err := NewSequentialWithState(
[]string{"a"}, 1, EndStop, NewUpstreamEmptyState(),
); err == nil {
t.Fatal("NewSequentialWithState(single upstream) error = nil")
}
}
func TestSequentialSharesUpstreamEmptyStateAcrossRoutingCursors(t *testing.T) { func TestSequentialSharesUpstreamEmptyStateAcrossRoutingCursors(t *testing.T) {
empty := NewUpstreamEmptyState() empty := NewUpstreamEmptyState()
first, err := NewSequentialWithState([]string{"a", "b"}, 5, EndStayLast, empty) first, err := NewSequentialWithState([]string{"a", "b"}, 5, EndStayLast, empty)
@ -122,15 +149,18 @@ func TestSequentialSharesUpstreamEmptyStateAcrossRoutingCursors(t *testing.T) {
} }
func TestSequentialStopEndBehaviorHasNoCurrentSelection(t *testing.T) { func TestSequentialStopEndBehaviorHasNoCurrentSelection(t *testing.T) {
sequence, err := NewSequentialWithState([]string{"a"}, 1, EndStop, NewUpstreamEmptyState()) sequence, err := NewSequentialWithState([]string{"a", "b"}, 1, EndStop, NewUpstreamEmptyState())
if err != nil { if err != nil {
t.Fatalf("NewSequentialWithState(): %v", err) t.Fatalf("NewSequentialWithState(): %v", err)
} }
if !sequence.ObserveEmpty("a") { if !sequence.ObserveEmpty("a") {
t.Fatal("ObserveEmpty() = false, want transition to stopped") t.Fatal("ObserveEmpty(a) = false, want transition to b")
} }
if current, ok, version := sequence.CurrentSelection(); ok || current != "" || version != 2 { if !sequence.ObserveEmpty("b") {
t.Fatalf("CurrentSelection() = %q, %v, %d; want stopped version 2", current, ok, version) t.Fatal("ObserveEmpty(b) = false, want transition to stopped")
}
if current, ok, version := sequence.CurrentSelection(); ok || current != "" || version != 3 {
t.Fatalf("CurrentSelection() = %q, %v, %d; want stopped version 3", current, ok, version)
} }
} }

View File

@ -132,7 +132,7 @@ type Sequential struct {
} }
func NewSequential(upstreams []string, threshold int) (*Sequential, error) { func NewSequential(upstreams []string, threshold int) (*Sequential, error) {
return NewSequentialWithState(upstreams, threshold, EndStayLast, NewUpstreamEmptyState()) return NewSequentialWithState(upstreams, threshold, EndStop, NewUpstreamEmptyState())
} }
func NewSequentialWithState( func NewSequentialWithState(
@ -141,14 +141,14 @@ func NewSequentialWithState(
end EndBehavior, end EndBehavior,
empty *UpstreamEmptyState, empty *UpstreamEmptyState,
) (*Sequential, error) { ) (*Sequential, error) {
if len(upstreams) == 0 { if len(upstreams) < 2 {
return nil, fmt.Errorf("sequential strategy requires at least one upstream") return nil, fmt.Errorf("sequential strategy requires at least two upstreams")
} }
if threshold <= 0 { if threshold <= 0 {
return nil, fmt.Errorf("sequential threshold must be greater than zero") return nil, fmt.Errorf("sequential threshold must be greater than zero")
} }
if end == "" { if end == "" {
end = EndStayLast end = EndStop
} }
if end != EndStayLast && end != EndStop && end != EndLoop { if end != EndStayLast && end != EndStop && end != EndLoop {
return nil, fmt.Errorf("sequential end behavior %q is invalid", end) return nil, fmt.Errorf("sequential end behavior %q is invalid", end)

View File

@ -2,6 +2,11 @@
## 2026-07-29 ## 2026-07-29
- 根据对话最终定稿统一 Sequential至少两个 Upstream省略 `endBehavior`
默认 `stop`新增领域和严格配置回归测试。disabled candidate、跨实例游标和
`onUnavailable` 请求链仍未提前标记完成。
- 一次并行读取误用了不存在的 `routing/sequential_test.go`,另一次误用了旧 ADR
文件名;已改用 `routing_test.go``006-postgresql-admin-state.md`,未重复命令。
- 新增文档契约测试,递归验证 README、docs、deploy、diagrams 的相对链接,并 - 新增文档契约测试,递归验证 README、docs、deploy、diagrams 的相对链接,并
拒绝公开指南引用不存在的具体 Go 命令目标;修正配置参考中尚未实现的 拒绝公开指南引用不存在的具体 Go 命令目标;修正配置参考中尚未实现的
`cmd/proxy-controller` 启动命令,改为当前真实可执行的严格配置校验工具。 `cmd/proxy-controller` 启动命令,改为当前真实可执行的严格配置校验工具。