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