From 53c34f31292a63106e10f7cad813a696ffba565b Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:31:13 -0500 Subject: [PATCH] fix(router): cap the adaptive active mux at adaptCap even under load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proactive-park was gated on !saturated, so it only trimmed the active set when the group was idle. During the uncap fill the pool establishes hundreds of legs — all born active — and the route-setup/keepalive traffic makes the group read as saturated, so the park stayed suppressed and the active set ballooned to dozens of legs (observed live: ACTIVE climbing 16->31 while standby froze), head-of-line-stalling the no-skip reorder buffer. Introduce a two-tier park floor: converge active down to the lean steady target (desiredActive) when idle, but ALWAYS cap it at adaptCap — the maximum the saturation-growth rule (4) would ever build — even under load. Legs born active beyond adaptCap are shed slowest-first regardless of saturation; a real bulk download already sits at adaptCap, so only born-active excess is parked, never the legs the download is using. bundle.wasm regenerated (TinyGo); parity_test green; new test asserts the cap holds under sustained saturation. --- pkg/router/policy/preset/preset_test.go | 47 +++++++++++++++++++++ pkg/router/policy/preset/tick.go | 23 +++++++++- pkg/router/policy/wasm/presets/bundle.wasm | Bin 576882 -> 576885 bytes 3 files changed, 68 insertions(+), 2 deletions(-) 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 7f335c2fd63a439c213aff3222fe4830b5064ac3..5a2ffa7218a26387fc07cf0c42f81c32d411ae21 100644 GIT binary patch delta 1237 zcmZWoO=uHA6yBNL$xAYuG_l&2MCt4%jZLj6#m2vjBPy*eD0maEBHEKDj}{c#Lxr{` zeQ6N!;6bpUw~8nV{vfE}!Gi}u!5%zRELuUWe{VO_6>*oxd-LX-H{bi2PmRKtM&V|c zU?0wv#8^o%5>8^1I3$5DHeV7@-p7{b4-dk*eX2#X5x-SGc^B*E)+s-w;{%>h9f}Mk z^1d_w`2q~`z78EHhojczRW9$h4SPcJJ%9fK+wbgABE?j@{%s%h$-?84Q0_11RLo1u z<~~>H3Llp$)T3@wFC-~ab3aZ&wB@;YZxXtCeJ@Ql|>rQ0djI6oHx=L0M>uuz@WIV5pzHjt3qp#rQi%^8HI6~s{i?F586I9BH zDn(g63(%xr82to0tDyQqpooA}iI-BUr2b?b(5%4n)Ceb&^%_e^D6t+qS%dCfONRZj)Q>GG^dn1pVs#!UyRT;dd@v7`%(&4%BtES{fg1L)2D`m- zF1j)ua3bV{t4AHxKH|q|UfXbJ28tUdT^09Ssa$Oh2UAGKZ~E*Z-%fFyn1NDjSoMDr zsN$m;*q*Mt#6Q)kM397oNrWToT-m%uhzu+z`xf*yR2 zdY~O`XC!suyXit1N9r`2EjU|;@nTlepr`f2D5@R4r)FJj;@zQIac~wk(18=PP+q_C zf8>>@zjFlK5?iIZuo{$dcJ%vj<;OK22-d2@7??cs%JX7c){VY$&X&R~g2@5OUf)+}o?T7?;BqFU7La}}wTzx6$eNiE~xsR3x(;-y3^ zldvn!TqN@RiY)>xIu)F+ih@}b5oZRVapQn;vUA*V%{=c9Ut03~f}&*V2~9?xFJeOh zI(VOr#|yBDt;13QCRq;UAe>=EyfX+J*a*HI1i?Iv4S|1tOl`LEe4J3u6k>V)BD7qM zEE5rX60zxs-HX_5Vk*{LWVs$%K85%!#JeHh!pR}<>_8Z$BX7m$L(rUEr<5`xrKq!4 zY6sz)5MN@h2qGCcnvo-^@eYp^VGXr@wg_7Zt`(sUKG?RXl53{D3r86%h6~!e}GMdh9BJPnk}aAh+v+nk3cP=g5be zZOR9loiUdtPWJxr`-nm+_ezKZj;ioG-WY-!{8$2u`8Z#K9T`6rB~PgGXwR4?oOtSWPV_1J^IEJv9>G%|3dn%q%0xU++q#^a?b|xklF?|_QxN`*3%*Mkb(AOTf=~5-+ zbQDw%&W<`i6{)FblBT9x@($I%@RTY<o27gdipzJwn`>2aa8VZ$TyYfE