Coverage for qreward / client / load_balancer.py: 100.00%
48 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
1"""Load balancing strategies for proxy selection."""
3import threading
4from enum import Enum
5from typing import List, Optional
8class LoadBalanceStrategy(Enum):
9 """Load balancing strategy enumeration."""
11 ROUND_ROBIN = "round_robin"
12 WEIGHTED_ROUND_ROBIN = "weighted_round_robin"
13 LEAST_CONNECTIONS = "least_connections"
16class RoundRobinSelector:
17 """Thread-safe round-robin selector over a list of keys.
19 Skips keys that are marked unhealthy.
20 """
22 def __init__(self) -> None:
23 self._index = 0
24 self._lock = threading.Lock()
26 def select(
27 self,
28 keys: List[str],
29 healthy_keys: set[str],
30 ) -> Optional[str]:
31 """Select the next healthy key in round-robin order.
33 Args:
34 keys: Ordered list of all proxy keys.
35 healthy_keys: Set of keys currently considered healthy.
37 Returns:
38 The selected key, or None if no healthy key exists.
39 """
40 if not keys or not healthy_keys:
41 return None
43 with self._lock:
44 total = len(keys)
45 for _ in range(total):
46 key = keys[self._index % total]
47 self._index += 1
48 if key in healthy_keys:
49 return key
51 return None
54class WeightedRoundRobinSelector:
55 """Thread-safe weighted round-robin selector.
57 Keys with higher weights are selected more frequently.
58 Uses the smooth weighted round-robin algorithm (Nginx-style).
59 """
61 def __init__(self) -> None:
62 self._current_weights: dict[str, int] = {}
63 self._lock = threading.Lock()
65 def update_weights(self, weights: dict[str, int]) -> None:
66 """Update the weight configuration.
68 Args:
69 weights: Mapping of key to weight (positive integer).
70 """
71 with self._lock:
72 for key in weights:
73 if key not in self._current_weights:
74 self._current_weights[key] = 0
75 removed = set(self._current_weights) - set(weights)
76 for key in removed:
77 del self._current_weights[key]
79 def select(
80 self,
81 keys: List[str],
82 healthy_keys: set[str],
83 weights: dict[str, int],
84 ) -> Optional[str]:
85 """Select the next healthy key using smooth weighted round-robin.
87 Args:
88 keys: Ordered list of all proxy keys.
89 healthy_keys: Set of keys currently considered healthy.
90 weights: Mapping of key to weight.
92 Returns:
93 The selected key, or None if no healthy key exists.
94 """
95 if not keys or not healthy_keys:
96 return None
98 with self._lock:
99 eligible = [k for k in keys if k in healthy_keys and k in weights]
100 if not eligible:
101 return None
103 total_weight = sum(weights[k] for k in eligible)
105 # Increase current weights by effective weight
106 for key in eligible:
107 self._current_weights.setdefault(key, 0)
108 self._current_weights[key] += weights[key]
110 # Select the key with the highest current weight
111 best_key = max(eligible, key=lambda k: self._current_weights[k])
113 # Decrease the selected key's current weight
114 self._current_weights[best_key] -= total_weight
116 return best_key