diff --git a/dnscrypt-proxy/config_loader.go b/dnscrypt-proxy/config_loader.go index 60e76746ee..b34b7aecda 100644 --- a/dnscrypt-proxy/config_loader.go +++ b/dnscrypt-proxy/config_loader.go @@ -201,23 +201,38 @@ func configureServerParams(proxy *Proxy, config *Config) { func configureLoadBalancing(proxy *Proxy, config *Config) { lbStrategy := LBStrategy(DefaultLBStrategy) switch lbStrategyStr := strings.ToLower(config.LBStrategy); lbStrategyStr { + case "": // default - WP2 is now the default strategy dlog.Noticef("Using default Weighted Power of Two (WP2) load balancing strategy") + case "p2": lbStrategy = LBStrategyP2{} case "ph": lbStrategy = LBStrategyPH{} - case "fastest": + case "fastest", "first": // "fastest" kept for backward compatibility with older configs - fallthrough - case "first": lbStrategy = LBStrategyFirst{} case "random": lbStrategy = LBStrategyRandom{} case "wp2": lbStrategy = LBStrategyWP2{} + case "wph": + lbStrategy = LBStrategyWPH{} + default: + // Support wpN (e.g. wp3) -> LBStrategyWPN{n} + if after, ok := strings.CutPrefix(lbStrategyStr, "wp"); ok { + n, err := strconv.ParseInt(after, 10, 32) + if err != nil || n <= 0 { + dlog.Warnf("Invalid load balancing strategy: [%s]", config.LBStrategy) + } else { + lbStrategy = LBStrategyWPN{n: int(n)} + } + break + } + + // Support pN (e.g. p3) -> LBStrategyPN{n} if after, ok := strings.CutPrefix(lbStrategyStr, "p"); ok { n, err := strconv.ParseInt(after, 10, 32) if err != nil || n <= 0 { @@ -229,10 +244,12 @@ func configureLoadBalancing(proxy *Proxy, config *Config) { dlog.Warnf("Unknown load balancing strategy: [%s]", config.LBStrategy) } } + proxy.serversInfo.lbStrategy = lbStrategy proxy.serversInfo.lbEstimator = config.LBEstimator } + // configurePlugins - Configures DNS plugins func configurePlugins(proxy *Proxy, config *Config) { // Configure listen addresses and paths diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index 8d53a79551..3e04ae2d4e 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -178,6 +178,8 @@ keepalive = 30 ## Load-balancing strategy: 'wp2' (default), 'p2', 'ph', 'p', 'first', or 'random' ## 'wp2' (default): Weighted Power of Two - selects the better performing server ## from two random candidates based on real-time RTT and success rates. +## 'wph': randomly selects the better performing server from fastest half of servers. +## 'wp': randomly selects the better performing server from fastest n servers. ## 'p2': Randomly choose 1 of the fastest 2 servers by latency. ## 'ph': Randomly choose from fastest half of servers. ## 'p': Randomly choose from fastest n servers (e.g., 'p3' for fastest 3). diff --git a/dnscrypt-proxy/serversInfo.go b/dnscrypt-proxy/serversInfo.go index 7cb0425a24..e214f09165 100644 --- a/dnscrypt-proxy/serversInfo.go +++ b/dnscrypt-proxy/serversInfo.go @@ -133,8 +133,8 @@ func (LBStrategyRandom) getActiveCount(serversCount int) int { type LBStrategyWP2 struct{} func (LBStrategyWP2) getCandidate(serversCount int) int { - // This function is not used for WP2 - getWeightedCandidate is used instead - // But we need to implement it to satisfy the LBStrategy interface + // This function is not used for WP2 - getWeightedCandidate is used instead. + // But we need to implement it to satisfy the LBStrategy interface. if serversCount <= 1 { return 0 } @@ -145,6 +145,30 @@ func (LBStrategyWP2) getActiveCount(serversCount int) int { return serversCount // All servers are considered active for WP2 } +// LBStrategyWPH is the weighted version of PH: it limits the candidate pool +// to the first half of the servers, then applies the WP2 two-choice scoring. +type LBStrategyWPH struct{} + +func (LBStrategyWPH) getCandidate(serversCount int) int { + return rand.Intn((serversCount + 1) / 2) +} + +func (LBStrategyWPH) getActiveCount(serversCount int) int { + return (serversCount + 1) / 2 +} + +// LBStrategyWPN is the weighted version of PN: it limits the candidate pool +// to the first N servers, then applies the WP2 two-choice scoring. +type LBStrategyWPN struct{ n int } + +func (s LBStrategyWPN) getCandidate(serversCount int) int { + return rand.Intn(Min(serversCount, s.n)) +} + +func (s LBStrategyWPN) getActiveCount(serversCount int) int { + return Min(serversCount, s.n) +} + var DefaultLBStrategy = LBStrategyWP2{} type DNSCryptRelay struct { @@ -460,10 +484,16 @@ func (serversInfo *ServersInfo) getOne() *ServerInfo { var candidate int - // Check if using WP2 strategy - if _, isWP2 := serversInfo.lbStrategy.(LBStrategyWP2); isWP2 { - candidate = serversInfo.getWeightedCandidate(serversCount) - } else { + // WP2, WPH, and WPN all use the weighted two-choice algorithm. + // WPH/WPN restrict the pool using their PH/PN active-count semantics. + switch strategy := serversInfo.lbStrategy.(type) { + case LBStrategyWP2: + candidate = serversInfo.getWeightedCandidate(strategy.getActiveCount(serversCount)) + case LBStrategyWPH: + candidate = serversInfo.getWeightedCandidate(strategy.getActiveCount(serversCount)) + case LBStrategyWPN: + candidate = serversInfo.getWeightedCandidate(strategy.getActiveCount(serversCount)) + default: candidate = serversInfo.lbStrategy.getCandidate(serversCount) if serversInfo.lbEstimator { serversInfo.estimatorUpdate(candidate) @@ -480,7 +510,8 @@ func (serversInfo *ServersInfo) getOne() *ServerInfo { return serverInfo } -// getWeightedCandidate implements the WP2 algorithm +// getWeightedCandidate applies the WP2 two-choice algorithm within the +// specified active server pool. func (serversInfo *ServersInfo) getWeightedCandidate(serversCount int) int { if serversCount <= 1 { return 0