test: cover capacity reservation lifecycle

This commit is contained in:
youfak 2026-07-29 21:23:34 +08:00
parent f46d79511f
commit 125740f58d
6 changed files with 204 additions and 4 deletions

View File

@ -84,6 +84,10 @@ test/{fixtures,integration,e2e,load}/
- [x] Prove with 1,000 concurrent goroutines that effective capacity is never exceeded. - [x] Prove with 1,000 concurrent goroutines that effective capacity is never exceeded.
- [ ] Add race coverage and duplicate-release invariant metrics hook. - [ ] Add race coverage and duplicate-release invariant metrics hook.
当前进度2026-07-29固定 Max 下的每 Proxy 打包 CAS、Cancel/Commit/Release
生命周期、重复终结、错误顺序和同一 Reservation 并发终结已通过领域测试;动态
降容契约、低基数不变量指标、Linux race 证据及短 TTL runtime 排空回收待完成。
## Task 4: Routing and Sequential Switching ## Task 4: Routing and Sequential Switching
**Files:** `internal/domain/routing/*.go`, corresponding tests **Files:** `internal/domain/routing/*.go`, corresponding tests

View File

@ -25,7 +25,8 @@
- `CFG-*`YAML v4 未知字段拒绝、监听保护、引用/上限/认证边界校验21 份 - `CFG-*`YAML v4 未知字段拒绝、监听保护、引用/上限/认证边界校验21 份
配置持续测试。 配置持续测试。
- `PROXY-* / CAP-*`唯一键、TTL 优先级、状态迁移与 Active/Reserved 打包 - `PROXY-* / CAP-*`唯一键、TTL 优先级、状态迁移与 Active/Reserved 打包
原子计数1,000 goroutine 不超卖测试。 原子计数1,000 goroutine 不超卖,以及 Cancel、重复终结、错误顺序、并发
Commit/Cancel/Release 计数守恒测试。
- `ROUTE-001 / ROUTE-004`:首条命中规则与进程内 Concurrent Sequential 单次 - `ROUTE-001 / ROUTE-004`:首条命中规则与进程内 Concurrent Sequential 单次
切换;策略运行时接线、持久化恢复和跨实例 CAS 尚未完成。 切换;策略运行时接线、持久化恢复和跨实例 CAS 尚未完成。
- `FETCH-005 / FETCH-006`Valid、Empty、DuplicateOnly、Error 分类。 - `FETCH-005 / FETCH-006`Valid、Empty、DuplicateOnly、Error 分类。

View File

@ -48,7 +48,8 @@
1. **并发容量**1000 协程争用同一 Proxy始终满足 1. **并发容量**1000 协程争用同一 Proxy始终满足
`active + reserved <= effectiveMaxConcurrency` `active + reserved <= effectiveMaxConcurrency`
2. **Reservation 生命周期**Dial 成功/失败、超时、取消和重复 Release 均不 2. **Reservation 生命周期**Dial 成功/失败、超时、取消和重复 Release 均不
泄漏或产生负计数。 泄漏或产生负计数。领域层已覆盖 Cancel、重复终结、错误顺序和同一
Reservation 并发终结Gateway Handler 覆盖建连失败、重试和请求取消。
3. **singleflight**100 个缺池信号只产生一个有效 Fetch 调度。 3. **singleflight**100 个缺池信号只产生一个有效 Fetch 调度。
4. **Provider 限流**requestInterval、maxInFlight、timeout、重试和 429 4. **Provider 限流**requestInterval、maxInFlight、timeout、重试和 429
`Retry-After` 在虚拟时钟下准确。 `Retry-After` 在虚拟时钟下准确。

View File

@ -122,8 +122,8 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
- 每个 Proxy ID 已有独立打包原子计数,固定 Max 下 1,000 并发不会超卖;这满足 - 每个 Proxy ID 已有独立打包原子计数,固定 Max 下 1,000 并发不会超卖;这满足
当前 Gateway 热路径的基本预留不变量。 当前 Gateway 热路径的基本预留不变量。
- Reservation 缺少 Cancel、重复终结、错误顺序和并发 Commit/Cancel 的完整领域 - Reservation 已补齐 Cancel、重复终结、错误顺序和并发 Commit/Cancel/Release
测试Gateway 也会忽略 Release/Cancel 错误,尚无低基数不变量观测 seam。 的领域测试Gateway 仍会忽略 Release/Cancel 错误,尚无低基数不变量观测 seam。
- `SetMax` 与 counters 分离更新;降到当前占用以下时会出现 overcommitted 状态, - `SetMax` 与 counters 分离更新;降到当前占用以下时会出现 overcommitted 状态,
需要先确定“拒绝降容”或“允许排空”的正式契约。 需要先确定“拒绝降容”或“允许排空”的正式契约。
- Snapshot Store 永久保留见过的 Proxy ID 对应 Capacity短 TTL、高换 IP 场景下 - Snapshot Store 永久保留见过的 Proxy ID 对应 Capacity短 TTL、高换 IP 场景下

View File

@ -0,0 +1,191 @@
package proxy
import (
"errors"
"sync"
"sync/atomic"
"testing"
)
func TestReservationCancelReleasesReservedCapacity(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Cancel(); err != nil {
t.Fatalf("Cancel() error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
if err := reservation.Cancel(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("second Cancel() error = %v, want ErrReservationFinished", err)
}
if err := reservation.Commit(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Commit() after Cancel error = %v, want ErrReservationFinished", err)
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Release() after Cancel error = %v, want ErrReservationFinished", err)
}
reused, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() after Cancel = false, want released slot")
}
if err := reused.Cancel(); err != nil {
t.Fatalf("reused Cancel() error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestReservationCommitAndReleaseAreSingleUse(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Commit(); err != nil {
t.Fatalf("Commit() error = %v", err)
}
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Commit(); !errors.Is(err, ErrReservationCommitted) {
t.Fatalf("second Commit() error = %v, want ErrReservationCommitted", err)
}
if err := reservation.Cancel(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Cancel() after Commit error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Release(); err != nil {
t.Fatalf("Release() error = %v", err)
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("second Release() error = %v, want ErrReservationFinished", err)
}
if err := reservation.Commit(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Commit() after Release error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestReleaseBeforeCommitDoesNotConsumeReservation(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Release() before Commit error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 0, 1)
if err := reservation.Cancel(); err != nil {
t.Fatalf("Cancel() after rejected Release error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestConcurrentReservationTerminationPreservesCounters(t *testing.T) {
for iteration := range 1_000 {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatalf("iteration %d Reserve() = false", iteration)
}
start := make(chan struct{})
results := make(chan terminationResult, 2)
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
<-start
results <- terminationResult{operation: "commit", err: reservation.Commit()}
}()
go func() {
defer wait.Done()
<-start
results <- terminationResult{operation: "cancel", err: reservation.Cancel()}
}()
close(start)
wait.Wait()
close(results)
var succeeded string
for result := range results {
if result.err == nil {
if succeeded != "" {
t.Fatalf("iteration %d operations %s and %s both succeeded", iteration, succeeded, result.operation)
}
succeeded = result.operation
continue
}
if !errors.Is(result.err, ErrReservationFinished) {
t.Fatalf("iteration %d %s error = %v, want ErrReservationFinished", iteration, result.operation, result.err)
}
}
switch succeeded {
case "commit":
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Release(); err != nil {
t.Fatalf("iteration %d Release() error = %v", iteration, err)
}
case "cancel":
assertCapacityCounters(t, capacity, 0, 0)
default:
t.Fatalf("iteration %d has no successful termination", iteration)
}
assertCapacityCounters(t, capacity, 0, 0)
}
}
func TestConcurrentReleaseSucceedsOnce(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Commit(); err != nil {
t.Fatalf("Commit() error = %v", err)
}
var succeeded atomic.Int64
var unexpected atomic.Int64
var wait sync.WaitGroup
for range 100 {
wait.Add(1)
go func() {
defer wait.Done()
err := reservation.Release()
switch {
case err == nil:
succeeded.Add(1)
case !errors.Is(err, ErrReservationFinished):
unexpected.Add(1)
}
}()
}
wait.Wait()
if succeeded.Load() != 1 || unexpected.Load() != 0 {
t.Fatalf("Release() results = success:%d unexpected:%d, want 1 and 0", succeeded.Load(), unexpected.Load())
}
assertCapacityCounters(t, capacity, 0, 0)
}
type terminationResult struct {
operation string
err error
}
func assertCapacityCounters(t *testing.T, capacity *Capacity, active, reserved int64) {
t.Helper()
if got := capacity.Active(); got != active {
t.Fatalf("Active() = %d, want %d", got, active)
}
if got := capacity.Reserved(); got != reserved {
t.Fatalf("Reserved() = %d, want %d", got, reserved)
}
}

View File

@ -25,6 +25,9 @@
- 架构证据审计确认 Provider 分布式 Leader、Health Reducer/Checker、Prometheus - 架构证据审计确认 Provider 分布式 Leader、Health Reducer/Checker、Prometheus
指标模块和四个生产命令仍缺实现;追踪矩阵已把这些条目的配置/领域基础与 指标模块和四个生产命令仍缺实现;追踪矩阵已把这些条目的配置/领域基础与
完整运行时证据拆开描述。 完整运行时证据拆开描述。
- 新增独立 Capacity 生命周期测试,覆盖 Cancel 后复用、重复 Commit/Cancel/
Release、Release-before-Commit、1,000 轮并发 Commit/Cancel 及 100 并发
ReleaseProxy、Dispatch、Snapshot、Gateway Server 定向测试全部通过。
- 一次 `rg` 同时包含不存在的 `cmd` 路径,以及两次使用 PowerShell 不展开的 - 一次 `rg` 同时包含不存在的 `cmd` 路径,以及两次使用 PowerShell 不展开的
通配路径,分别返回退出码 2/123后续改用实际目录和 `-g` 过滤,不重复原命令。 通配路径,分别返回退出码 2/123后续改用实际目录和 `-g` 过滤,不重复原命令。
- 已实现共享 `httpapi`、`httpsecurity` 与 `httpserver`,统一严格 JSON、Problem、 - 已实现共享 `httpapi`、`httpsecurity` 与 `httpserver`,统一严格 JSON、Problem、