1#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum LoadBalanceStrategy {
9 RoundRobin,
11 LeastConnections,
13 WeightedRoundRobin,
15 ResourceAware,
17 ConsistentHash,
20}
21
22#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct WorkerLoad {
26 pub worker_id: u64,
27 pub connections: u32,
28 pub cpu_pct: f64,
29 pub memory_pct: f64,
30 pub weight: u32,
31}
32
33impl WorkerLoad {
34 #[must_use]
36 pub fn load_score(&self) -> f64 {
37 let conn_pct = f64::from(self.connections).min(100.0);
40 (0.5 * self.cpu_pct + 0.3 * self.memory_pct + 0.2 * conn_pct) / 100.0
41 }
42
43 #[must_use]
45 pub fn is_overloaded(&self) -> bool {
46 self.load_score() > 0.9
47 }
48}
49
50#[allow(dead_code)]
52pub struct LoadBalancer {
53 pub strategy: LoadBalanceStrategy,
54 pub workers: Vec<WorkerLoad>,
55 pub round_robin_idx: usize,
56}
57
58impl LoadBalancer {
59 #[must_use]
61 pub fn new(strategy: LoadBalanceStrategy) -> Self {
62 Self {
63 strategy,
64 workers: Vec::new(),
65 round_robin_idx: 0,
66 }
67 }
68
69 pub fn add_worker(&mut self, w: WorkerLoad) {
71 self.workers.push(w);
72 }
73
74 pub fn remove_worker(&mut self, id: u64) -> bool {
76 let before = self.workers.len();
77 self.workers.retain(|w| w.worker_id != id);
78 self.workers.len() < before
79 }
80
81 pub fn select_worker(&mut self) -> Option<u64> {
85 if self.workers.is_empty() {
86 return None;
87 }
88 match &self.strategy {
89 LoadBalanceStrategy::RoundRobin => {
90 let idx = self.round_robin_idx % self.workers.len();
91 self.round_robin_idx = self.round_robin_idx.wrapping_add(1);
92 Some(self.workers[idx].worker_id)
93 }
94 LoadBalanceStrategy::LeastConnections => self
95 .workers
96 .iter()
97 .min_by_key(|w| w.connections)
98 .map(|w| w.worker_id),
99 LoadBalanceStrategy::WeightedRoundRobin => {
100 let total_weight: u32 = self.workers.iter().map(|w| w.weight).sum();
102 if total_weight == 0 {
103 return Some(self.workers[0].worker_id);
104 }
105 let idx = self.round_robin_idx % total_weight as usize;
106 self.round_robin_idx = self.round_robin_idx.wrapping_add(1);
107 let mut acc = 0usize;
108 for w in &self.workers {
109 acc += w.weight as usize;
110 if idx < acc {
111 return Some(w.worker_id);
112 }
113 }
114 self.workers.last().map(|w| w.worker_id)
115 }
116 LoadBalanceStrategy::ResourceAware => self
117 .workers
118 .iter()
119 .min_by(|a, b| {
120 a.load_score()
121 .partial_cmp(&b.load_score())
122 .unwrap_or(std::cmp::Ordering::Equal)
123 })
124 .map(|w| w.worker_id),
125 LoadBalanceStrategy::ConsistentHash => {
126 let key = self.round_robin_idx as u64;
128 self.round_robin_idx = self.round_robin_idx.wrapping_add(1);
129 consistent_hash(key, &self.workers)
130 }
131 }
132 }
133
134 pub fn update_load(&mut self, worker_id: u64, connections: u32, cpu_pct: f64) {
136 if let Some(w) = self.workers.iter_mut().find(|w| w.worker_id == worker_id) {
137 w.connections = connections;
138 w.cpu_pct = cpu_pct;
139 }
140 }
141}
142
143#[must_use]
147pub fn consistent_hash(key: u64, workers: &[WorkerLoad]) -> Option<u64> {
148 if workers.is_empty() {
149 return None;
150 }
151 let mut h = key;
153 h ^= h >> 33;
154 h = h.wrapping_mul(0xff51afd7ed558ccd);
155 h ^= h >> 33;
156 h = h.wrapping_mul(0xc4ceb9fe1a85ec53);
157 h ^= h >> 33;
158 let idx = (h as usize) % workers.len();
159 Some(workers[idx].worker_id)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn worker(id: u64, conns: u32, cpu: f64, mem: f64, weight: u32) -> WorkerLoad {
167 WorkerLoad {
168 worker_id: id,
169 connections: conns,
170 cpu_pct: cpu,
171 memory_pct: mem,
172 weight,
173 }
174 }
175
176 #[test]
177 fn test_load_score_zero_load() {
178 let w = worker(1, 0, 0.0, 0.0, 1);
179 assert_eq!(w.load_score(), 0.0);
180 }
181
182 #[test]
183 fn test_load_score_full_load() {
184 let w = worker(1, 100, 100.0, 100.0, 1);
185 assert!((w.load_score() - 1.0).abs() < 1e-9);
186 }
187
188 #[test]
189 fn test_is_overloaded() {
190 let ok = worker(1, 10, 50.0, 50.0, 1);
191 assert!(!ok.is_overloaded());
192 let hot = worker(2, 100, 100.0, 100.0, 1);
193 assert!(hot.is_overloaded());
194 }
195
196 #[test]
197 fn test_no_workers_returns_none() {
198 let mut lb = LoadBalancer::new(LoadBalanceStrategy::RoundRobin);
199 assert!(lb.select_worker().is_none());
200 }
201
202 #[test]
203 fn test_round_robin_cycles() {
204 let mut lb = LoadBalancer::new(LoadBalanceStrategy::RoundRobin);
205 lb.add_worker(worker(1, 0, 0.0, 0.0, 1));
206 lb.add_worker(worker(2, 0, 0.0, 0.0, 1));
207 let first = lb.select_worker().expect("worker selection should succeed");
208 let second = lb.select_worker().expect("worker selection should succeed");
209 let third = lb.select_worker().expect("worker selection should succeed");
210 assert_ne!(first, second);
211 assert_eq!(first, third);
212 }
213
214 #[test]
215 fn test_least_connections() {
216 let mut lb = LoadBalancer::new(LoadBalanceStrategy::LeastConnections);
217 lb.add_worker(worker(1, 10, 0.0, 0.0, 1));
218 lb.add_worker(worker(2, 2, 0.0, 0.0, 1));
219 lb.add_worker(worker(3, 7, 0.0, 0.0, 1));
220 assert_eq!(
221 lb.select_worker().expect("worker selection should succeed"),
222 2
223 );
224 }
225
226 #[test]
227 fn test_resource_aware_picks_lowest_load() {
228 let mut lb = LoadBalancer::new(LoadBalanceStrategy::ResourceAware);
229 lb.add_worker(worker(1, 50, 80.0, 70.0, 1)); lb.add_worker(worker(2, 0, 5.0, 5.0, 1)); assert_eq!(
232 lb.select_worker().expect("worker selection should succeed"),
233 2
234 );
235 }
236
237 #[test]
238 fn test_remove_worker() {
239 let mut lb = LoadBalancer::new(LoadBalanceStrategy::RoundRobin);
240 lb.add_worker(worker(1, 0, 0.0, 0.0, 1));
241 lb.add_worker(worker(2, 0, 0.0, 0.0, 1));
242 assert!(lb.remove_worker(1));
243 assert!(!lb.remove_worker(99)); assert_eq!(lb.workers.len(), 1);
245 assert_eq!(lb.workers[0].worker_id, 2);
246 }
247
248 #[test]
249 fn test_update_load() {
250 let mut lb = LoadBalancer::new(LoadBalanceStrategy::RoundRobin);
251 lb.add_worker(worker(1, 0, 0.0, 0.0, 1));
252 lb.update_load(1, 42, 75.0);
253 assert_eq!(lb.workers[0].connections, 42);
254 assert!((lb.workers[0].cpu_pct - 75.0).abs() < f64::EPSILON);
255 }
256
257 #[test]
258 fn test_weighted_round_robin() {
259 let mut lb = LoadBalancer::new(LoadBalanceStrategy::WeightedRoundRobin);
260 lb.add_worker(worker(1, 0, 0.0, 0.0, 3));
261 lb.add_worker(worker(2, 0, 0.0, 0.0, 1));
262 let results: Vec<u64> = (0..4)
264 .map(|_| lb.select_worker().expect("worker selection should succeed"))
265 .collect();
266 let count_1 = results.iter().filter(|&&id| id == 1).count();
267 let count_2 = results.iter().filter(|&&id| id == 2).count();
268 assert_eq!(count_1, 3);
269 assert_eq!(count_2, 1);
270 }
271
272 #[test]
273 fn test_consistent_hash_stable() {
274 let workers = vec![worker(10, 0, 0.0, 0.0, 1), worker(20, 0, 0.0, 0.0, 1)];
275 let r1 = consistent_hash(42, &workers);
276 let r2 = consistent_hash(42, &workers);
277 assert_eq!(r1, r2);
278 }
279
280 #[test]
281 fn test_consistent_hash_empty() {
282 assert!(consistent_hash(0, &[]).is_none());
283 }
284
285 #[test]
286 fn test_consistent_hash_single_worker() {
287 let workers = vec![worker(99, 0, 0.0, 0.0, 1)];
288 assert_eq!(consistent_hash(12345, &workers), Some(99));
289 }
290
291 #[test]
292 fn test_add_multiple_workers() {
293 let mut lb = LoadBalancer::new(LoadBalanceStrategy::RoundRobin);
294 for i in 1..=5 {
295 lb.add_worker(worker(i, 0, 0.0, 0.0, 1));
296 }
297 assert_eq!(lb.workers.len(), 5);
298 }
299}