Skip to main content

postrust_proxy/vendored/
backend.rs

1//! Vendored backend management from rpxy-lib: backend/*.rs
2//!
3//! This module provides load balancing and upstream management.
4
5use 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/// Load balance context for sticky sessions and weighted selection.
15#[derive(Clone, Debug)]
16pub struct LoadBalanceContext {
17    /// Client IP for sticky sessions
18    pub client_ip: Option<std::net::IpAddr>,
19    /// Cookie value for sticky sessions
20    pub sticky_cookie: Option<String>,
21}
22
23/// Pointer to a selected upstream backend.
24#[derive(Clone, Debug)]
25pub struct PointerToUpstream {
26    /// Index in the backends array
27    pub ptr: usize,
28    /// Context for sticky sessions
29    pub context: Option<String>,
30}
31
32/// Load balancer trait.
33pub trait LoadBalanceWithPointer: Send + Sync {
34    /// Get pointer to the next upstream backend.
35    fn get_ptr(&self, ctx: Option<&LoadBalanceContext>) -> PointerToUpstream;
36}
37
38/// Round-robin load balancer (from rpxy load_balance_main.rs).
39pub 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
66/// Random load balancer (from rpxy load_balance_main.rs).
67pub 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
84/// Least connections load balancer (our addition).
85pub 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    /// Increment connection count for a backend.
103    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    /// Decrement connection count for a backend.
110    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
139/// Weighted load balancer (our addition).
140pub 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        // Fallback to last backend
172        PointerToUpstream {
173            ptr: self.weights.len() - 1,
174            context: None,
175        }
176    }
177}
178
179/// Load balancer enum (adapted from rpxy).
180pub 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                // For now, fall back to round-robin. Sticky requires cookie handling.
204                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
219/// Backend application manager (adapted from rpxy backend_main.rs).
220pub struct BackendAppManager {
221    /// Upstreams by ID
222    upstreams: DashMap<Uuid, UpstreamEntry>,
223    /// Route matcher: (host, path_prefix) -> upstream_id
224    routes: DashMap<(ServerName, PathName), Uuid>,
225    /// Health checker reference
226    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    /// Register an upstream.
249    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    /// Register a route.
264    pub fn register_route(&self, host: ServerName, path: PathName, upstream_id: Uuid) {
265        self.routes.insert((host, path), upstream_id);
266    }
267
268    /// Find the best matching upstream for a request.
269    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        // Find all matching routes and select the one with longest path prefix
274        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    /// Select a backend from an upstream, considering health status.
291    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        // Get healthy backends
304        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 // No health checker or no ID means assume healthy
313                }
314            })
315            .collect();
316
317        if healthy_backends.is_empty() {
318            // All backends unhealthy, fall back to first backend
319            return Some(upstream.backends[0].clone());
320        }
321
322        // Use load balancer to select
323        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    /// Get upstream by ID.
329    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); // Wraps around
352    }
353
354    #[test]
355    fn test_random() {
356        let lb = LoadBalanceRandom::new(10);
357
358        // Just verify it returns valid indices
359        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        // Run many iterations and check distribution
370        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        // Backend 2 should get roughly 70% of traffic
377        assert!(counts[2] > counts[0] && counts[2] > counts[1]);
378    }
379}