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 {
34 fn get_ptr(&self, ctx: Option<&LoadBalanceContext>) -> PointerToUpstream;
36}
37
38pub struct LoadBalanceRoundRobin {
40 ptr: Arc<AtomicUsize>,
41 num_upstreams: usize,
42}
43
44impl LoadBalanceRoundRobin {
45 pub fn new(num_upstreams: usize) -> Self {
47 Self {
48 ptr: Arc::new(AtomicUsize::new(0)),
49 num_upstreams,
50 }
51 }
52}
53
54impl LoadBalanceWithPointer for LoadBalanceRoundRobin {
55 fn get_ptr(&self, _ctx: Option<&LoadBalanceContext>) -> PointerToUpstream {
56 let current = self.ptr.load(Ordering::Relaxed);
57 let next = (current + 1) % self.num_upstreams;
58 self.ptr.store(next, Ordering::Relaxed);
59
60 PointerToUpstream {
61 ptr: current,
62 context: None,
63 }
64 }
65}
66
67pub struct LoadBalanceRandom {
69 num_upstreams: usize,
70}
71
72impl LoadBalanceRandom {
73 pub fn new(num_upstreams: usize) -> Self {
75 Self { num_upstreams }
76 }
77}
78
79impl LoadBalanceWithPointer for LoadBalanceRandom {
80 fn get_ptr(&self, _ctx: Option<&LoadBalanceContext>) -> PointerToUpstream {
81 let ptr = rand::rng().random_range(0..self.num_upstreams);
82 PointerToUpstream { ptr, context: None }
83 }
84}
85
86pub struct LoadBalanceLeastConn {
88 connections: Arc<DashMap<usize, AtomicUsize>>,
89 num_upstreams: usize,
90}
91
92impl LoadBalanceLeastConn {
93 pub fn new(num_upstreams: usize) -> Self {
94 let connections = Arc::new(DashMap::new());
95 for i in 0..num_upstreams {
96 connections.insert(i, AtomicUsize::new(0));
97 }
98 Self {
99 connections,
100 num_upstreams,
101 }
102 }
103
104 pub fn increment(&self, idx: usize) {
106 if let Some(count) = self.connections.get(&idx) {
107 count.fetch_add(1, Ordering::Relaxed);
108 }
109 }
110
111 pub fn decrement(&self, idx: usize) {
113 if let Some(count) = self.connections.get(&idx) {
114 count.fetch_sub(1, Ordering::Relaxed);
115 }
116 }
117}
118
119impl LoadBalanceWithPointer for LoadBalanceLeastConn {
120 fn get_ptr(&self, _ctx: Option<&LoadBalanceContext>) -> PointerToUpstream {
121 let mut min_idx = 0;
122 let mut min_conns = usize::MAX;
123
124 for i in 0..self.num_upstreams {
125 if let Some(count) = self.connections.get(&i) {
126 let conns = count.load(Ordering::Relaxed);
127 if conns < min_conns {
128 min_conns = conns;
129 min_idx = i;
130 }
131 }
132 }
133
134 PointerToUpstream {
135 ptr: min_idx,
136 context: None,
137 }
138 }
139}
140
141pub struct LoadBalanceWeighted {
143 weights: Vec<u32>,
144 total_weight: u32,
145}
146
147impl LoadBalanceWeighted {
148 pub fn new(weights: Vec<u32>) -> Self {
149 let total_weight = weights.iter().sum();
150 Self {
151 weights,
152 total_weight,
153 }
154 }
155}
156
157impl LoadBalanceWithPointer for LoadBalanceWeighted {
158 fn get_ptr(&self, _ctx: Option<&LoadBalanceContext>) -> PointerToUpstream {
159 let mut rng = rand::rng();
160 let random = rng.random_range(0..self.total_weight);
161
162 let mut cumulative = 0;
163 for (idx, weight) in self.weights.iter().enumerate() {
164 cumulative += weight;
165 if random < cumulative {
166 return PointerToUpstream {
167 ptr: idx,
168 context: None,
169 };
170 }
171 }
172
173 PointerToUpstream {
175 ptr: self.weights.len() - 1,
176 context: None,
177 }
178 }
179}
180
181pub enum LoadBalance {
183 RoundRobin(LoadBalanceRoundRobin),
185 Random(LoadBalanceRandom),
187 LeastConnections(LoadBalanceLeastConn),
189 Weighted(LoadBalanceWeighted),
191}
192
193impl LoadBalance {
194 pub fn from_strategy(strategy: &LoadBalanceStrategy, backends: &[Backend]) -> Self {
199 let num = backends.len();
200 match strategy {
201 LoadBalanceStrategy::RoundRobin => {
202 LoadBalance::RoundRobin(LoadBalanceRoundRobin::new(num))
203 }
204 LoadBalanceStrategy::Random => LoadBalance::Random(LoadBalanceRandom::new(num)),
205 LoadBalanceStrategy::LeastConnections => {
206 LoadBalance::LeastConnections(LoadBalanceLeastConn::new(num))
207 }
208 LoadBalanceStrategy::Weighted => {
209 let weights: Vec<u32> = backends.iter().map(|b| b.weight).collect();
210 LoadBalance::Weighted(LoadBalanceWeighted::new(weights))
211 }
212 LoadBalanceStrategy::Sticky => {
213 LoadBalance::RoundRobin(LoadBalanceRoundRobin::new(num))
215 }
216 }
217 }
218
219 pub fn get_ptr(&self, ctx: Option<&LoadBalanceContext>) -> PointerToUpstream {
221 match self {
222 LoadBalance::RoundRobin(lb) => lb.get_ptr(ctx),
223 LoadBalance::Random(lb) => lb.get_ptr(ctx),
224 LoadBalance::LeastConnections(lb) => lb.get_ptr(ctx),
225 LoadBalance::Weighted(lb) => lb.get_ptr(ctx),
226 }
227 }
228}
229
230pub struct BackendAppManager {
232 upstreams: DashMap<Uuid, UpstreamEntry>,
234 routes: DashMap<(ServerName, PathName), Uuid>,
236 health_checker: Option<Arc<HealthChecker>>,
238}
239
240struct UpstreamEntry {
241 upstream: Upstream,
242 load_balance: LoadBalance,
243}
244
245impl BackendAppManager {
246 pub fn new() -> Self {
248 Self {
249 upstreams: DashMap::new(),
250 routes: DashMap::new(),
251 health_checker: None,
252 }
253 }
254
255 pub fn with_health_checker(mut self, checker: Arc<HealthChecker>) -> Self {
257 self.health_checker = Some(checker);
258 self
259 }
260
261 pub fn register_upstream(&self, upstream: Upstream) {
263 let id = upstream.resolved_id();
264 let load_balance = LoadBalance::from_strategy(&upstream.lb_strategy, &upstream.backends);
265 self.upstreams.insert(
266 id,
267 UpstreamEntry {
268 upstream,
269 load_balance,
270 },
271 );
272 }
273
274 pub fn register_route(&self, host: ServerName, path: PathName, upstream_id: Uuid) {
276 self.routes.insert((host, path), upstream_id);
277 }
278
279 pub fn select_backend(
281 &self,
282 upstream_id: Uuid,
283 ctx: Option<&LoadBalanceContext>,
284 ) -> Option<Backend> {
285 let entry = self.upstreams.get(&upstream_id)?;
286 let upstream = &entry.upstream;
287
288 if upstream.backends.is_empty() {
289 return None;
290 }
291
292 let healthy_backends: Vec<(usize, &Backend)> = upstream
294 .backends
295 .iter()
296 .enumerate()
297 .filter(|(_, b)| {
298 if let (Some(checker), Some(id)) = (&self.health_checker, b.id) {
299 checker.is_healthy(id)
300 } else {
301 true }
303 })
304 .collect();
305
306 if healthy_backends.is_empty() {
307 return Some(upstream.backends[0].clone());
309 }
310
311 let ptr = entry.load_balance.get_ptr(ctx);
313 let idx = ptr.ptr % healthy_backends.len();
314 Some(healthy_backends[idx].1.clone())
315 }
316
317 pub fn get_upstream(&self, id: Uuid) -> Option<Upstream> {
319 self.upstreams.get(&id).map(|e| e.upstream.clone())
320 }
321}
322
323impl Default for BackendAppManager {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_round_robin() {
335 let lb = LoadBalanceRoundRobin::new(3);
336
337 assert_eq!(lb.get_ptr(None).ptr, 0);
338 assert_eq!(lb.get_ptr(None).ptr, 1);
339 assert_eq!(lb.get_ptr(None).ptr, 2);
340 assert_eq!(lb.get_ptr(None).ptr, 0); }
342
343 #[test]
344 fn test_random() {
345 let lb = LoadBalanceRandom::new(10);
346
347 for _ in 0..100 {
349 let ptr = lb.get_ptr(None).ptr;
350 assert!(ptr < 10);
351 }
352 }
353
354 #[test]
355 fn test_weighted() {
356 let lb = LoadBalanceWeighted::new(vec![1, 2, 7]);
357
358 let mut counts = [0u32; 3];
360 for _ in 0..1000 {
361 let ptr = lb.get_ptr(None).ptr;
362 counts[ptr] += 1;
363 }
364
365 assert!(counts[2] > counts[0] && counts[2] > counts[1]);
367 }
368}