@@ -35,6 +35,7 @@ const (
3535 molsFallbackRTTThreshold = 2 * time .Second
3636 molsMinActiveNodes = 2
3737 defaultMaxActiveRelays = 3
38+ molsP2CPressureDelta = 0.3
3839)
3940
4041func molsScore (i , j , m1 , m2 , order int ) int {
@@ -105,7 +106,8 @@ func molsCongestionScore(i, j, m1, m2, order int) int {
105106 return (order * order + 1 ) - molsScore (i , (order - 1 )- j , m1 , m2 , order )
106107}
107108
108- // hashToGridIndex maps an identity string to a stable FNV-1a hash. Callers
109+ // hashToGridIndex maps an identity string to a stable FNV-1a hash with a 2nd-stage
110+ // bit-mixing cascade (avalanche diffusion to eliminate clustering). Callers
109111// fold it into the current grid order with % order; the folded index is not
110112// stable across orders, so it is recomputed whenever the pool size changes.
111113func hashToGridIndex (s string ) uint32 {
@@ -114,6 +116,11 @@ func hashToGridIndex(s string) uint32 {
114116 h ^= uint32 (s [i ])
115117 h *= 16777619
116118 }
119+ h ^= h >> 16
120+ h *= 0x85ebca6b
121+ h ^= h >> 13
122+ h *= 0xc2b2ae35
123+ h ^= h >> 16
117124 return h
118125}
119126
@@ -286,17 +293,33 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
286293 return 0
287294 })
288295
289- tierOut := make ([]string , 0 , len (candidates ))
296+ var nonSaturated []molsCandidate
297+ var saturated []molsCandidate
290298 for _ , candidate := range candidates {
291- if ! candidate .state .IsSaturated {
292- tierOut = append (tierOut , candidate .state .Descriptor .APIHTTPSAddr )
299+ if candidate .state .IsSaturated {
300+ saturated = append (saturated , candidate )
301+ } else {
302+ nonSaturated = append (nonSaturated , candidate )
293303 }
294304 }
295- for _ , candidate := range candidates {
296- if candidate .state .IsSaturated {
297- tierOut = append (tierOut , candidate .state .Descriptor .APIHTTPSAddr )
305+
306+ // P2C pressure optimization: If candidate 0 has significantly higher
307+ // pressure than candidate 1, swap them to relieve node pressure without herd effect.
308+ if len (nonSaturated ) >= 2 {
309+ p0 := nonSaturated [0 ].state .Pressure ()
310+ p1 := nonSaturated [1 ].state .Pressure ()
311+ if p0 - p1 > molsP2CPressureDelta {
312+ nonSaturated [0 ], nonSaturated [1 ] = nonSaturated [1 ], nonSaturated [0 ]
298313 }
299314 }
315+
316+ tierOut := make ([]string , 0 , len (candidates ))
317+ for _ , candidate := range nonSaturated {
318+ tierOut = append (tierOut , candidate .state .Descriptor .APIHTTPSAddr )
319+ }
320+ for _ , candidate := range saturated {
321+ tierOut = append (tierOut , candidate .state .Descriptor .APIHTTPSAddr )
322+ }
300323 return tierOut
301324 }
302325
@@ -327,8 +350,41 @@ func SelectPriority(states []RelayState, routeState RouteState) []string {
327350 if maxActive <= 0 {
328351 maxActive = defaultMaxActiveRelays
329352 }
330- if len (auto ) > maxActive {
331- auto = auto [:maxActive ]
332- }
353+ auto = applyActiveStickiness (auto , routeState .ActiveRelayURLs , maxActive )
333354 return append (explicit , auto ... )
334355}
356+
357+ // applyActiveStickiness preserves currently active relays that remain in the
358+ // ranked eligible pool to avoid connection churn.
359+ func applyActiveStickiness (ranked []string , activeRelayURLs []string , maxActive int ) []string {
360+ if len (activeRelayURLs ) == 0 || len (ranked ) <= maxActive {
361+ if len (ranked ) > maxActive {
362+ return ranked [:maxActive ]
363+ }
364+ return ranked
365+ }
366+ activeSet := make (map [string ]struct {}, len (activeRelayURLs ))
367+ for _ , u := range activeRelayURLs {
368+ activeSet [u ] = struct {}{}
369+ }
370+ selected := make ([]string , 0 , maxActive )
371+ for _ , u := range ranked {
372+ if _ , isActive := activeSet [u ]; isActive {
373+ selected = append (selected , u )
374+ if len (selected ) == maxActive {
375+ break
376+ }
377+ }
378+ }
379+ if len (selected ) < maxActive {
380+ for _ , u := range ranked {
381+ if ! slices .Contains (selected , u ) {
382+ selected = append (selected , u )
383+ if len (selected ) == maxActive {
384+ break
385+ }
386+ }
387+ }
388+ }
389+ return selected
390+ }
0 commit comments