@@ -21,6 +21,7 @@ import (
2121 "cmp"
2222 "math"
2323 "slices"
24+ "strconv"
2425 "time"
2526)
2627
@@ -31,7 +32,7 @@ const (
3132 molsVariantM2 uint8 = 11
3233
3334 molsCongestionRTTThreshold = 500 * time .Millisecond
34- molsCVThreshold = 0.5
35+ molsCVThreshold = 0.6
3536 molsFallbackRTTThreshold = 2 * time .Second
3637 molsMinActiveNodes = 2
3738 defaultMaxActiveRelays = 3
@@ -132,7 +133,7 @@ func molsRTTStats(states []RelayState) (mean time.Duration, cv float64) {
132133 continue
133134 }
134135 count ++
135- sum += float64 (state .DiscoveryRTT )
136+ sum += float64 (state .effectiveRTT () )
136137 }
137138 if count == 0 {
138139 return 0 , 0
@@ -146,7 +147,7 @@ func molsRTTStats(states []RelayState) (mean time.Duration, cv float64) {
146147 if state .DiscoveryRTTAt .IsZero () {
147148 continue
148149 }
149- d := float64 (state .DiscoveryRTT ) - avg
150+ d := float64 (state .effectiveRTT () ) - avg
150151 sq += d * d
151152 }
152153 stddev := math .Sqrt (sq / float64 (count ))
@@ -157,7 +158,7 @@ func molsRTTStats(states []RelayState) (mean time.Duration, cv float64) {
157158}
158159
159160func isRelayFallback (state RelayState ) bool {
160- return ! state .DiscoveryRTTAt .IsZero () && state .DiscoveryRTT > molsFallbackRTTThreshold
161+ return ! state .DiscoveryRTTAt .IsZero () && state .effectiveRTT () > molsFallbackRTTThreshold
161162}
162163
163164type molsCandidate struct {
@@ -201,9 +202,9 @@ func selectConfirmed(states []RelayState) []RelayState {
201202 return out
202203}
203204
204- // RankRelayPool ranks the autoPool of relay states using MOLS selection for the given local address.
205+ // RankRelayPool ranks the autoPool of relay states using MOLS selection for the given local address and epoch .
205206// The returned slice contains relay URLs ordered by MOLS-derived priority with saturation partitioning.
206- func RankRelayPool (autoPool []RelayState , localAddress string ) []string {
207+ func RankRelayPool (autoPool []RelayState , localAddress string , epoch uint64 ) []string {
207208 if len (autoPool ) == 0 {
208209 return nil
209210 }
@@ -214,7 +215,11 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
214215
215216 order := len (autoPool )
216217 m1 , m2 , _ := molsMultipliers (order , nonLinear )
217- ingressRow := int (hashToGridIndex (localAddress ) % uint32 (order ))
218+ ingressKey := localAddress
219+ if epoch > 0 {
220+ ingressKey = localAddress + "#" + strconv .FormatUint (epoch , 10 )
221+ }
222+ ingressRow := int (hashToGridIndex (ingressKey ) % uint32 (order ))
218223
219224 type relayHash struct {
220225 url string
@@ -259,10 +264,12 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
259264
260265 if len (activeStates ) < molsMinActiveNodes && len (fallbackStates ) > 0 {
261266 slices .SortFunc (fallbackStates , func (a , b RelayState ) int {
262- if a .DiscoveryRTT < b .DiscoveryRTT {
267+ aRTT := a .effectiveRTT ()
268+ bRTT := b .effectiveRTT ()
269+ if aRTT < bRTT {
263270 return - 1
264271 }
265- if a . DiscoveryRTT > b . DiscoveryRTT {
272+ if aRTT > bRTT {
266273 return 1
267274 }
268275 return 0
@@ -305,14 +312,27 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
305312 }
306313 }
307314
308- // P2C pressure optimization: If candidate 0 has significantly higher
309- // pressure than candidate 1, swap them to relieve node pressure without herd effect.
315+ // Pressure-aware partitioning: Candidates with significantly elevated pressure
316+ // (pressure difference > molsP2CPressureDelta compared to minimum pressure) are
317+ // demoted behind low-pressure candidates so they are pushed outside the MaxActiveRelays
318+ // quota, achieving real active-set membership migration.
310319 if len (nonSaturated ) >= 2 {
311- p0 := nonSaturated [0 ].state .Pressure ()
312- p1 := nonSaturated [1 ].state .Pressure ()
313- if p0 - p1 > molsP2CPressureDelta {
314- nonSaturated [0 ], nonSaturated [1 ] = nonSaturated [1 ], nonSaturated [0 ]
320+ minPressure := nonSaturated [0 ].state .Pressure ()
321+ for _ , c := range nonSaturated [1 :] {
322+ if p := c .state .Pressure (); p < minPressure {
323+ minPressure = p
324+ }
315325 }
326+ var lowPressure []molsCandidate
327+ var highPressure []molsCandidate
328+ for _ , c := range nonSaturated {
329+ if c .state .Pressure ()- minPressure > molsP2CPressureDelta {
330+ highPressure = append (highPressure , c )
331+ } else {
332+ lowPressure = append (lowPressure , c )
333+ }
334+ }
335+ nonSaturated = append (lowPressure , highPressure ... )
316336 }
317337
318338 tierOut := make ([]string , 0 , len (candidates ))
@@ -347,29 +367,55 @@ func SelectPriority(states []RelayState, routeState RouteState) []string {
347367 }
348368 explicit = append (explicit , relayURL )
349369 }
350- auto := RankRelayPool (filterCandidatePool (states , routeState , now , false ), routeState .LocalAddress )
370+ auto := RankRelayPool (filterCandidatePool (states , routeState , now , false ), routeState .LocalAddress , routeState . SelectionEpoch )
351371 maxActive := routeState .MaxActiveRelays
352372 if maxActive <= 0 {
353373 maxActive = defaultMaxActiveRelays
354374 }
355- auto = applyActiveStickiness (auto , routeState .ActiveRelayURLs , maxActive )
375+ auto = applyActiveStickiness (auto , routeState .ActiveRelayURLs , states , maxActive )
376+ if len (auto ) > maxActive {
377+ auto = auto [:maxActive ]
378+ }
356379 return append (explicit , auto ... )
357380}
358381
359- // applyActiveStickiness preserves currently active relays that remain in the
360- // ranked eligible pool to avoid connection churn.
361- func applyActiveStickiness (ranked []string , activeRelayURLs []string , maxActive int ) []string {
362- if len (activeRelayURLs ) == 0 || len (ranked ) <= maxActive {
363- if len (ranked ) > maxActive {
364- return ranked [:maxActive ]
365- }
382+ // applyActiveStickiness reorders the ranked candidates so that eligible healthy sticky
383+ // relays occupy the first maxActive positions without dropping remaining pool candidates.
384+ //
385+ // Priority cascade:
386+ //
387+ // Layer 1: Sticky relays — preserves currently active connections ONLY if they remain in
388+ // the healthy tier (not saturated and not in fallback) to avoid zombie resurrection.
389+ // Layer 2: Warm healthy candidates — fills remaining slots using top-ranked MOLS candidates.
390+ // Trailing: Remaining ranked candidates are preserved to support multi-hop path building.
391+ func applyActiveStickiness (ranked []string , activeRelayURLs []string , states []RelayState , maxActive int ) []string {
392+ if len (ranked ) == 0 || maxActive <= 0 {
393+ return ranked
394+ }
395+ if len (activeRelayURLs ) == 0 {
366396 return ranked
367397 }
398+
399+ stateMap := make (map [string ]RelayState , len (states ))
400+ for _ , s := range states {
401+ stateMap [s .Descriptor .APIHTTPSAddr ] = s
402+ }
403+
404+ // Stickiness is ONLY granted to healthy nodes: non-saturated and non-fallback.
405+ // Degraded/saturated nodes must migrate out rather than being resurrected.
368406 activeSet := make (map [string ]struct {}, len (activeRelayURLs ))
369407 for _ , u := range activeRelayURLs {
370- activeSet [u ] = struct {}{}
408+ if s , ok := stateMap [u ]; ok {
409+ s .EvaluateSaturation ()
410+ if s .IsSaturated || isRelayFallback (s ) {
411+ continue
412+ }
413+ activeSet [u ] = struct {}{}
414+ }
371415 }
372- selected := make ([]string , 0 , maxActive )
416+
417+ selected := make ([]string , 0 , len (ranked ))
418+ // Layer 1: Retain currently active sticky relays that remain healthy (capped at maxActive)
373419 for _ , u := range ranked {
374420 if _ , isActive := activeSet [u ]; isActive {
375421 selected = append (selected , u )
@@ -378,14 +424,10 @@ func applyActiveStickiness(ranked []string, activeRelayURLs []string, maxActive
378424 }
379425 }
380426 }
381- if len (selected ) < maxActive {
382- for _ , u := range ranked {
383- if ! slices .Contains (selected , u ) {
384- selected = append (selected , u )
385- if len (selected ) == maxActive {
386- break
387- }
388- }
427+ // Layer 2 & Trailing: Append remaining candidates preserving their relative MOLS ranking
428+ for _ , u := range ranked {
429+ if ! slices .Contains (selected , u ) {
430+ selected = append (selected , u )
389431 }
390432 }
391433 return selected
0 commit comments