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 let id = upstream.resolved_id();
251 let load_balance = LoadBalance::from_strategy(&upstream.lb_strategy, &upstream.backends);
252 self.upstreams.insert(
253 id,
254 UpstreamEntry {
255 upstream,
256 load_balance,
257 },
258 );
259 }
260
261 pub fn register_route(&self, host: ServerName, path: PathName, upstream_id: Uuid) {
263 self.routes.insert((host, path), upstream_id);
264 }
265
266 pub fn find_upstream(&self, host: &str, path: &str) -> Option<Uuid> {
268 let _host_name = ServerName::new(host);
269 let _path_name = PathName::new(path);
270
271 let mut best_match: Option<(usize, Uuid)> = None;
273
274 for entry in self.routes.iter() {
275 let ((route_host, route_path), upstream_id) = entry.pair();
276
277 if route_host.matches(host) && route_path.matches(path) {
278 let path_len = route_path.len();
279 if best_match.is_none() || path_len > best_match.unwrap().0 {
280 best_match = Some((path_len, *upstream_id));
281 }
282 }
283 }
284
285 best_match.map(|(_, id)| id)
286 }
287
288 pub fn select_backend(
290 &self,
291 upstream_id: Uuid,
292 ctx: Option<&LoadBalanceContext>,
293 ) -> Option<Backend> {
294 let entry = self.upstreams.get(&upstream_id)?;
295 let upstream = &entry.upstream;
296
297 if upstream.backends.is_empty() {
298 return None;
299 }
300
301 let healthy_backends: Vec<(usize, &Backend)> = upstream
303 .backends
304 .iter()
305 .enumerate()
306 .filter(|(_, b)| {
307 if let (Some(checker), Some(id)) = (&self.health_checker, b.id) {
308 checker.is_healthy(id)
309 } else {
310 true }
312 })
313 .collect();
314
315 if healthy_backends.is_empty() {
316 return Some(upstream.backends[0].clone());
318 }
319
320 let ptr = entry.load_balance.get_ptr(ctx);
322 let idx = ptr.ptr % healthy_backends.len();
323 Some(healthy_backends[idx].1.clone())
324 }
325
326 pub fn get_upstream(&self, id: Uuid) -> Option<Upstream> {
328 self.upstreams.get(&id).map(|e| e.upstream.clone())
329 }
330}
331
332impl Default for BackendAppManager {
333 fn default() -> Self {
334 Self::new()
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_round_robin() {
344 let lb = LoadBalanceRoundRobin::new(3);
345
346 assert_eq!(lb.get_ptr(None).ptr, 0);
347 assert_eq!(lb.get_ptr(None).ptr, 1);
348 assert_eq!(lb.get_ptr(None).ptr, 2);
349 assert_eq!(lb.get_ptr(None).ptr, 0); }
351
352 #[test]
353 fn test_random() {
354 let lb = LoadBalanceRandom::new(10);
355
356 for _ in 0..100 {
358 let ptr = lb.get_ptr(None).ptr;
359 assert!(ptr < 10);
360 }
361 }
362
363 #[test]
364 fn test_weighted() {
365 let lb = LoadBalanceWeighted::new(vec![1, 2, 7]);
366
367 let mut counts = [0u32; 3];
369 for _ in 0..1000 {
370 let ptr = lb.get_ptr(None).ptr;
371 counts[ptr] += 1;
372 }
373
374 assert!(counts[2] > counts[0] && counts[2] > counts[1]);
376 }
377}