1use crate::config::{Backend, LoadBalanceStrategy, Upstream};
6use crate::health::HealthChecker;
7use crate::vendored::types::{PathName, ServerName};
8use dashmap::DashMap;
9use rand::Rng;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::Arc;
12use uuid::Uuid;
13
14#[derive(Clone, Debug)]
16pub struct LoadBalanceContext {
17 pub client_ip: Option<std::net::IpAddr>,
19 pub sticky_cookie: Option<String>,
21}
22
23#[derive(Clone, Debug)]
25pub struct PointerToUpstream {
26 pub ptr: usize,
28 pub context: Option<String>,
30}
31
32pub trait LoadBalanceWithPointer: Send + Sync {
40 fn get_ptr(
43 &self,
44 ctx: Option<&LoadBalanceContext>,
45 eligible: &[usize],
46 ) -> Option<PointerToUpstream>;
47}
48
49fn pointer(ptr: usize) -> Option<PointerToUpstream> {
50 Some(PointerToUpstream { ptr, context: None })
51}
52
53#[derive(Default)]
55pub struct LoadBalanceRoundRobin {
56 ptr: AtomicUsize,
57}
58
59impl LoadBalanceRoundRobin {
60 pub fn new() -> Self {
62 Self::default()
63 }
64}
65
66impl LoadBalanceWithPointer for LoadBalanceRoundRobin {
67 fn get_ptr(
68 &self,
69 _ctx: Option<&LoadBalanceContext>,
70 eligible: &[usize],
71 ) -> Option<PointerToUpstream> {
72 if eligible.is_empty() {
73 return None;
74 }
75 let turn = self.ptr.fetch_add(1, Ordering::Relaxed);
78 pointer(eligible[turn % eligible.len()])
79 }
80}
81
82#[derive(Default)]
84pub struct LoadBalanceRandom;
85
86impl LoadBalanceRandom {
87 pub fn new() -> Self {
89 Self
90 }
91}
92
93impl LoadBalanceWithPointer for LoadBalanceRandom {
94 fn get_ptr(
95 &self,
96 _ctx: Option<&LoadBalanceContext>,
97 eligible: &[usize],
98 ) -> Option<PointerToUpstream> {
99 if eligible.is_empty() {
100 return None;
101 }
102 pointer(eligible[rand::rng().random_range(0..eligible.len())])
103 }
104}
105
106pub struct LoadBalanceLeastConn {
108 connections: Arc<DashMap<usize, AtomicUsize>>,
109 start: AtomicUsize,
113}
114
115impl LoadBalanceLeastConn {
116 pub fn new(num_upstreams: usize) -> Self {
118 let connections = Arc::new(DashMap::new());
119 for i in 0..num_upstreams {
120 connections.insert(i, AtomicUsize::new(0));
121 }
122 Self {
123 connections,
124 start: AtomicUsize::new(0),
125 }
126 }
127
128 pub fn increment(&self, idx: usize) {
130 if let Some(count) = self.connections.get(&idx) {
131 count.fetch_add(1, Ordering::Relaxed);
132 }
133 }
134
135 pub fn decrement(&self, idx: usize) {
137 if let Some(count) = self.connections.get(&idx) {
138 count.fetch_sub(1, Ordering::Relaxed);
139 }
140 }
141}
142
143impl LoadBalanceWithPointer for LoadBalanceLeastConn {
144 fn get_ptr(
145 &self,
146 _ctx: Option<&LoadBalanceContext>,
147 eligible: &[usize],
148 ) -> Option<PointerToUpstream> {
149 if eligible.is_empty() {
150 return None;
151 }
152 let start = self.start.fetch_add(1, Ordering::Relaxed);
153 let mut best: Option<(usize, usize)> = None;
154 for offset in 0..eligible.len() {
155 let idx = eligible[(start + offset) % eligible.len()];
156 let conns = self
157 .connections
158 .get(&idx)
159 .map(|c| c.load(Ordering::Relaxed))
160 .unwrap_or(0);
161 if best.is_none_or(|(_, fewest)| conns < fewest) {
162 best = Some((idx, conns));
163 }
164 }
165 best.and_then(|(idx, _)| pointer(idx))
166 }
167}
168
169pub struct LoadBalanceWeighted {
171 weights: Vec<u32>,
173}
174
175impl LoadBalanceWeighted {
176 pub fn new(weights: Vec<u32>) -> Self {
179 Self { weights }
180 }
181}
182
183impl LoadBalanceWithPointer for LoadBalanceWeighted {
184 fn get_ptr(
185 &self,
186 _ctx: Option<&LoadBalanceContext>,
187 eligible: &[usize],
188 ) -> Option<PointerToUpstream> {
189 if eligible.is_empty() {
190 return None;
191 }
192 let weight = |idx: usize| u64::from(self.weights.get(idx).copied().unwrap_or(0));
193
194 let total: u64 = eligible.iter().map(|&idx| weight(idx)).sum();
198
199 if total == 0 {
204 return pointer(eligible[rand::rng().random_range(0..eligible.len())]);
205 }
206
207 let mut point = rand::rng().random_range(0..total);
208 for &idx in eligible {
209 let w = weight(idx);
210 if point < w {
211 return pointer(idx);
212 }
213 point -= w;
214 }
215 eligible.last().copied().and_then(pointer)
218 }
219}
220
221pub enum LoadBalance {
223 RoundRobin(LoadBalanceRoundRobin),
225 Random(LoadBalanceRandom),
227 LeastConnections(LoadBalanceLeastConn),
229 Weighted(LoadBalanceWeighted),
231}
232
233impl LoadBalance {
234 pub fn from_strategy(strategy: &LoadBalanceStrategy, backends: &[Backend]) -> Self {
239 let num = backends.len();
240 match strategy {
241 LoadBalanceStrategy::RoundRobin => {
242 LoadBalance::RoundRobin(LoadBalanceRoundRobin::new())
243 }
244 LoadBalanceStrategy::Random => LoadBalance::Random(LoadBalanceRandom::new()),
245 LoadBalanceStrategy::LeastConnections => {
246 LoadBalance::LeastConnections(LoadBalanceLeastConn::new(num))
247 }
248 LoadBalanceStrategy::Weighted => {
249 let weights: Vec<u32> = backends.iter().map(|b| b.weight).collect();
250 LoadBalance::Weighted(LoadBalanceWeighted::new(weights))
251 }
252 LoadBalanceStrategy::Sticky => {
253 LoadBalance::RoundRobin(LoadBalanceRoundRobin::new())
255 }
256 }
257 }
258
259 pub fn get_ptr(
262 &self,
263 ctx: Option<&LoadBalanceContext>,
264 eligible: &[usize],
265 ) -> Option<PointerToUpstream> {
266 match self {
267 LoadBalance::RoundRobin(lb) => lb.get_ptr(ctx, eligible),
268 LoadBalance::Random(lb) => lb.get_ptr(ctx, eligible),
269 LoadBalance::LeastConnections(lb) => lb.get_ptr(ctx, eligible),
270 LoadBalance::Weighted(lb) => lb.get_ptr(ctx, eligible),
271 }
272 }
273}
274
275pub struct BackendAppManager {
277 upstreams: DashMap<Uuid, UpstreamEntry>,
279 routes: DashMap<(ServerName, PathName), Uuid>,
281 health_checker: Option<Arc<HealthChecker>>,
283}
284
285struct UpstreamEntry {
286 upstream: Upstream,
287 load_balance: LoadBalance,
288}
289
290impl BackendAppManager {
291 pub fn new() -> Self {
293 Self {
294 upstreams: DashMap::new(),
295 routes: DashMap::new(),
296 health_checker: None,
297 }
298 }
299
300 pub fn with_health_checker(mut self, checker: Arc<HealthChecker>) -> Self {
302 self.health_checker = Some(checker);
303 self
304 }
305
306 pub fn register_upstream(&self, upstream: Upstream) {
308 let id = upstream.resolved_id();
309 let load_balance = LoadBalance::from_strategy(&upstream.lb_strategy, &upstream.backends);
310 self.upstreams.insert(
311 id,
312 UpstreamEntry {
313 upstream,
314 load_balance,
315 },
316 );
317 }
318
319 pub fn register_route(&self, host: ServerName, path: PathName, upstream_id: Uuid) {
321 self.routes.insert((host, path), upstream_id);
322 }
323
324 pub fn select_backend(
332 &self,
333 upstream_id: Uuid,
334 ctx: Option<&LoadBalanceContext>,
335 ) -> Option<Backend> {
336 let entry = self.upstreams.get(&upstream_id)?;
337 let upstream = &entry.upstream;
338
339 if !upstream.enabled {
340 return None;
341 }
342
343 let enabled: Vec<usize> = upstream
344 .backends
345 .iter()
346 .enumerate()
347 .filter(|(_, b)| b.enabled)
348 .map(|(idx, _)| idx)
349 .collect();
350
351 let healthy: Vec<usize> = enabled
352 .iter()
353 .copied()
354 .filter(
355 |&idx| match (&self.health_checker, upstream.backends[idx].id) {
356 (Some(checker), Some(id)) => checker.is_healthy(id),
357 _ => true,
359 },
360 )
361 .collect();
362
363 let candidates = if healthy.is_empty() {
372 &enabled
373 } else {
374 &healthy
375 };
376
377 let ptr = entry.load_balance.get_ptr(ctx, candidates)?;
378 upstream.backends.get(ptr.ptr).cloned()
379 }
380
381 pub fn get_upstream(&self, id: Uuid) -> Option<Upstream> {
383 self.upstreams.get(&id).map(|e| e.upstream.clone())
384 }
385}
386
387impl Default for BackendAppManager {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::config::{HealthCheckConfig, UpstreamHttpVersion};
397
398 #[test]
399 fn test_round_robin() {
400 let lb = LoadBalanceRoundRobin::new();
401 let all = [0, 1, 2];
402
403 assert_eq!(lb.get_ptr(None, &all).unwrap().ptr, 0);
404 assert_eq!(lb.get_ptr(None, &all).unwrap().ptr, 1);
405 assert_eq!(lb.get_ptr(None, &all).unwrap().ptr, 2);
406 assert_eq!(lb.get_ptr(None, &all).unwrap().ptr, 0); }
408
409 #[test]
410 fn test_random() {
411 let lb = LoadBalanceRandom::new();
412 let eligible = [1, 4, 7];
413
414 for _ in 0..100 {
416 let ptr = lb.get_ptr(None, &eligible).unwrap().ptr;
417 assert!(eligible.contains(&ptr));
418 }
419 }
420
421 #[test]
422 fn test_weighted() {
423 let lb = LoadBalanceWeighted::new(vec![1, 2, 7]);
424
425 let mut counts = [0u32; 3];
427 for _ in 0..1000 {
428 let ptr = lb.get_ptr(None, &[0, 1, 2]).unwrap().ptr;
429 counts[ptr] += 1;
430 }
431
432 assert!(counts[2] > counts[0] && counts[2] > counts[1]);
434 }
435
436 #[test]
437 fn all_zero_weights_are_equal_weights_not_a_panic() {
438 let lb = LoadBalanceWeighted::new(vec![0, 0, 0]);
440 let mut seen = [false; 3];
441 for _ in 0..300 {
442 seen[lb.get_ptr(None, &[0, 1, 2]).unwrap().ptr] = true;
443 }
444 assert_eq!(seen, [true, true, true]);
445 }
446
447 #[test]
448 fn weights_are_over_the_eligible_backends() {
449 let lb = LoadBalanceWeighted::new(vec![1, 3, 1000]);
454 let mut counts = [0u32; 3];
455 for _ in 0..4000 {
456 counts[lb.get_ptr(None, &[0, 1]).unwrap().ptr] += 1;
457 }
458 assert_eq!(counts[2], 0);
459 assert!(counts[1] > counts[0] * 2, "{counts:?}");
461 }
462
463 #[test]
464 fn a_zero_weight_beside_others_gets_nothing() {
465 let lb = LoadBalanceWeighted::new(vec![0, 5]);
466 for _ in 0..200 {
467 assert_eq!(lb.get_ptr(None, &[0, 1]).unwrap().ptr, 1);
468 }
469 }
470
471 #[test]
472 fn no_eligible_backend_is_no_pick() {
473 assert!(LoadBalanceRoundRobin::new().get_ptr(None, &[]).is_none());
474 assert!(LoadBalanceRandom::new().get_ptr(None, &[]).is_none());
475 assert!(LoadBalanceLeastConn::new(2).get_ptr(None, &[]).is_none());
476 assert!(LoadBalanceWeighted::new(vec![1])
477 .get_ptr(None, &[])
478 .is_none());
479 }
480
481 #[test]
482 fn least_connections_rotates_through_ties() {
483 let lb = LoadBalanceLeastConn::new(3);
484 let mut seen = [false; 3];
485 for _ in 0..3 {
486 seen[lb.get_ptr(None, &[0, 1, 2]).unwrap().ptr] = true;
487 }
488 assert_eq!(seen, [true, true, true]);
489
490 lb.increment(0);
491 lb.increment(1);
492 for _ in 0..5 {
493 assert_eq!(lb.get_ptr(None, &[0, 1, 2]).unwrap().ptr, 2);
494 }
495 }
496
497 fn backend(address: &str, enabled: bool) -> Backend {
498 Backend {
499 id: Some(Uuid::new_v4()),
500 address: address.into(),
501 scheme: "http".into(),
502 weight: 1,
503 enabled,
504 http_version: UpstreamHttpVersion::Http11,
505 }
506 }
507
508 fn upstream(backends: Vec<Backend>, enabled: bool) -> Upstream {
509 Upstream {
510 id: Some(Uuid::new_v4()),
511 name: "u".into(),
512 description: None,
513 lb_strategy: LoadBalanceStrategy::RoundRobin,
514 backends,
515 health_check: HealthCheckConfig::default(),
516 enabled,
517 }
518 }
519
520 #[test]
521 fn a_disabled_backend_is_never_selected() {
522 let manager = BackendAppManager::new();
523 let up = upstream(vec![backend("off:1", false), backend("on:1", true)], true);
524 let id = up.resolved_id();
525 manager.register_upstream(up);
526 for _ in 0..10 {
527 assert_eq!(manager.select_backend(id, None).unwrap().address, "on:1");
528 }
529 }
530
531 #[test]
532 fn no_enabled_backend_is_no_backend() {
533 let manager = BackendAppManager::new();
534 let up = upstream(vec![backend("off:1", false), backend("off:2", false)], true);
535 let id = up.resolved_id();
536 manager.register_upstream(up);
537 assert!(manager.select_backend(id, None).is_none());
538 }
539
540 #[test]
541 fn a_disabled_upstream_has_no_backend() {
542 let manager = BackendAppManager::new();
543 let up = upstream(vec![backend("on:1", true)], false);
544 let id = up.resolved_id();
545 manager.register_upstream(up);
546 assert!(manager.select_backend(id, None).is_none());
547 }
548
549 #[test]
550 fn all_unhealthy_falls_back_to_enabled_backends_only() {
551 let checker = Arc::new(HealthChecker::new());
552 let disabled = backend("off:1", false);
553 let a = backend("sick:1", true);
554 let b = backend("sick:2", true);
555 for b in [&disabled, &a, &b] {
556 checker.mark_unhealthy_for_test(b.id.unwrap());
557 }
558
559 let manager = BackendAppManager::new().with_health_checker(checker);
560 let up = upstream(vec![disabled, a, b], true);
561 let id = up.resolved_id();
562 manager.register_upstream(up);
563
564 let mut seen = std::collections::HashSet::new();
565 for _ in 0..10 {
566 seen.insert(manager.select_backend(id, None).unwrap().address);
567 }
568 assert_eq!(
571 seen,
572 ["sick:1".to_string(), "sick:2".to_string()]
573 .into_iter()
574 .collect()
575 );
576 }
577
578 #[test]
579 fn a_healthy_backend_is_preferred() {
580 let checker = Arc::new(HealthChecker::new());
581 let sick = backend("sick:1", true);
582 let well = backend("well:1", true);
583 checker.mark_unhealthy_for_test(sick.id.unwrap());
584
585 let manager = BackendAppManager::new().with_health_checker(checker);
586 let up = upstream(vec![sick, well], true);
587 let id = up.resolved_id();
588 manager.register_upstream(up);
589 for _ in 0..10 {
590 assert_eq!(manager.select_backend(id, None).unwrap().address, "well:1");
591 }
592 }
593}