diff --git a/pkg/router/policy/preset/preset_test.go b/pkg/router/policy/preset/preset_test.go index eb927494ec..0b84240030 100644 --- a/pkg/router/policy/preset/preset_test.go +++ b/pkg/router/policy/preset/preset_test.go @@ -270,6 +270,53 @@ func TestEngine_OnTick_AdaptiveEvictsLowThroughputLeg(t *testing.T) { } } +// TestEngine_OnTick_AdaptiveCapsActiveUnderLoad asserts the active set is capped +// at adaptCap even when the group reads SATURATED. During the uncap fill, legs +// are born active and route-setup traffic keeps the group saturated — the park +// must still cap active at adaptCap (parking the slowest excess) so the reorder +// buffer never spans more than adaptCap legs; only born-active excess is shed, +// never below adaptCap under load. +func TestEngine_OnTick_AdaptiveCapsActiveUnderLoad(t *testing.T) { + e := seedSaturatedAdaptive(1) + const n = 20 + legs := make([]LegInfo, n) + for i := range legs { + tid := string(rune('a' + i)) + var recv uint64 + if i == 0 { + tid = "t0" // matches seedSaturatedAdaptive's prevRecv key + recv = 1_000_000 // fresh delta keeps the group saturated this tick + } + // distinct latencies: leg i = 20+i*10 ms, so higher index = slower. + legs[i] = LegInfo{Index: i, TransportID: tid, Kind: "stcpr", LatencyMs: 20 + i*10, Alive: true, RecvBytes: recv} + } + act := e.OnTick("adaptive", legs) + wantParked := n - adaptCap + if len(act.DemoteToStandby) != wantParked { + t.Fatalf("under load, active must be capped at adaptCap=%d: want %d parked, got %d: %+v", + adaptCap, wantParked, len(act.DemoteToStandby), act) + } + parked := map[int]bool{} + for _, idx := range act.DemoteToStandby { + if idx == 0 { + t.Fatalf("must never park leg 0; got %+v", act.DemoteToStandby) + } + parked[idx] = true + } + // The adaptCap kept-active legs are the fastest (leg 0 + legs 1..adaptCap-1); + // the n-adaptCap slowest (highest index) are parked. + for i := adaptCap; i < n; i++ { + if !parked[i] { + t.Errorf("slowest excess legs must be parked to cap active at adaptCap: leg %d (%dms) not parked", i, 20+i*10) + } + } + for i := 1; i < adaptCap; i++ { + if parked[i] { + t.Errorf("fastest legs must stay active under load: leg %d (%dms) was parked", i, 20+i*10) + } + } +} + func TestDecide_TimeOfDay(t *testing.T) { const h = int64(3600) * 1_000_000_000 if got := Decide("time-of-day", Context{NowUnixNano: 11 * h}, nil); got.Mux != 1 { diff --git a/pkg/router/policy/preset/tick.go b/pkg/router/policy/preset/tick.go index ef27082c0a..2b90f0311b 100644 --- a/pkg/router/policy/preset/tick.go +++ b/pkg/router/policy/preset/tick.go @@ -970,7 +970,26 @@ func (e *Engine) tickAdaptive(legs []LegInfo) RotationAction { // Cooldown is set after a bulk park so the freshly-lean set settles before // any optimization reshape; drop-recovery (1) and unhealthy-evict (2) still // bypass it, so safety is preserved. - if !saturated && !fwdSaturated && aliveCount > desiredActive && standbyCount < adaptStandbyMax && newestAliveIdx > 0 { + // + // PARK FLOOR — the active width we converge DOWN to this tick: + // - idle (not saturated): desiredActive, the lean steady target, so an + // interactive/idle flow settles onto the fastest few legs. + // - under load (saturated): adaptCap — the MAXIMUM the saturation-growth + // rule (4) would ever build. This is the fix for the uncap fill: legs are + // born ACTIVE (self-heal filling the 512-deep reserve), and while the pool + // fills, the route-setup/keepalive traffic reads as "saturated" — so a + // park gated on !saturated stayed suppressed and aliveCount ballooned to + // dozens of active legs (HoL-stalling the no-skip reorder buffer) even + // though rule 4 would never grow the active set beyond adaptCap. So the + // park ALWAYS caps active at adaptCap, and only trims below it (to + // desiredActive) when genuinely idle. Under a real bulk download active is + // already at adaptCap, so this only sheds born-active excess, never the + // legs the download is using. + parkFloor := desiredActive + if (saturated || fwdSaturated) && parkFloor < adaptCap { + parkFloor = adaptCap + } + if aliveCount > parkFloor && standbyCount < adaptStandbyMax && newestAliveIdx > 0 { // Rank active non-leg-0 legs by smoothed latency, slowest first. Use the // EWMA where we have it, else the raw sample; an unmeasured leg sorts as // slowest (parked first) so unknowns don't linger in the active set. @@ -993,7 +1012,7 @@ func (e *Engine) tickAdaptive(legs []LegInfo) RotationAction { act = append(act, actLeg{l.Index, lat}) } sort.Slice(act, func(i, j int) bool { return act[i].lat > act[j].lat }) - surplus := aliveCount - desiredActive + surplus := aliveCount - parkFloor if room := adaptStandbyMax - standbyCount; surplus > room { surplus = room } diff --git a/pkg/router/policy/wasm/presets/bundle.wasm b/pkg/router/policy/wasm/presets/bundle.wasm index 7f335c2fd6..5a2ffa7218 100644 Binary files a/pkg/router/policy/wasm/presets/bundle.wasm and b/pkg/router/policy/wasm/presets/bundle.wasm differ