fix: align sequential routing defaults
This commit is contained in:
parent
35a201cab1
commit
2dbfe2f742
@ -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`。
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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。
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -115,8 +115,8 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
|
|||||||
503;wait/direct 和默认 reject 尚未形成运行时闭环。
|
503;wait/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/连接
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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` 启动命令,改为当前真实可执行的严格配置校验工具。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user