rskit_discovery/
balancer.rs1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use dashmap::DashMap;
4
5use crate::instance::ServiceInstance;
6
7pub trait LoadBalancer: Send + Sync {
9 fn pick<'a>(&self, instances: &'a [ServiceInstance]) -> Option<&'a ServiceInstance>;
11}
12
13pub struct RoundRobin {
15 counter: AtomicUsize,
16}
17
18impl RoundRobin {
19 pub fn new() -> Self {
21 Self {
22 counter: AtomicUsize::new(0),
23 }
24 }
25}
26
27impl Default for RoundRobin {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl LoadBalancer for RoundRobin {
34 fn pick<'a>(&self, instances: &'a [ServiceInstance]) -> Option<&'a ServiceInstance> {
35 if instances.is_empty() {
36 return None;
37 }
38 let idx = self.counter.fetch_add(1, Ordering::Relaxed) % instances.len();
39 Some(&instances[idx])
40 }
41}
42
43pub struct Random;
45
46impl LoadBalancer for Random {
47 fn pick<'a>(&self, instances: &'a [ServiceInstance]) -> Option<&'a ServiceInstance> {
48 if instances.is_empty() {
49 return None;
50 }
51 use rand::Rng;
52 let idx = rand::rng().random_range(0..instances.len());
53 Some(&instances[idx])
54 }
55}
56
57#[derive(Debug, Clone, Copy, Default)]
59pub struct Weighted;
60
61impl LoadBalancer for Weighted {
62 fn pick<'a>(&self, instances: &'a [ServiceInstance]) -> Option<&'a ServiceInstance> {
63 if instances.is_empty() {
64 return None;
65 }
66
67 let total = instances
68 .iter()
69 .map(|instance| u64::from(instance.weight.max(1)))
70 .sum::<u64>();
71 use rand::Rng;
72 let mut slot = rand::rng().random_range(0..total);
73 for instance in instances {
74 let weight = u64::from(instance.weight.max(1));
75 if slot < weight {
76 return Some(instance);
77 }
78 slot -= weight;
79 }
80 instances.last()
81 }
82}
83
84pub struct LeastConnections {
86 in_flight: DashMap<String, AtomicUsize>,
87}
88
89impl LeastConnections {
90 pub fn new() -> Self {
92 Self {
93 in_flight: DashMap::new(),
94 }
95 }
96
97 pub fn acquire(&self, id: &str) {
99 self.in_flight
100 .entry(id.to_owned())
101 .or_insert_with(|| AtomicUsize::new(0))
102 .fetch_add(1, Ordering::Relaxed);
103 }
104
105 pub fn release(&self, id: &str) {
107 if let Some(counter) = self.in_flight.get(id) {
108 counter.fetch_sub(1, Ordering::Relaxed);
109 }
110 }
111}
112
113impl Default for LeastConnections {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl LoadBalancer for LeastConnections {
120 fn pick<'a>(&self, instances: &'a [ServiceInstance]) -> Option<&'a ServiceInstance> {
121 if instances.is_empty() {
122 return None;
123 }
124 instances.iter().min_by_key(|inst| {
125 self.in_flight
126 .get(&inst.id)
127 .map(|c| c.load(Ordering::Relaxed))
128 .unwrap_or(0)
129 })
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use std::collections::HashMap;
137
138 fn instance(id: &str) -> ServiceInstance {
139 ServiceInstance {
140 id: id.into(),
141 name: "svc".into(),
142 address: "127.0.0.1".into(),
143 port: 8080,
144 healthy: true,
145 weight: 1,
146 tags: vec![],
147 metadata: HashMap::new(),
148 }
149 }
150
151 #[test]
152 fn round_robin_cycles() {
153 let rr = RoundRobin::new();
154 let instances = vec![instance("a"), instance("b"), instance("c")];
155
156 assert_eq!(rr.pick(&instances).unwrap().id, "a");
157 assert_eq!(rr.pick(&instances).unwrap().id, "b");
158 assert_eq!(rr.pick(&instances).unwrap().id, "c");
159 assert_eq!(rr.pick(&instances).unwrap().id, "a");
160 }
161
162 #[test]
163 fn round_robin_empty() {
164 let rr = RoundRobin::new();
165 assert!(rr.pick(&[]).is_none());
166 }
167
168 #[test]
169 fn random_non_empty() {
170 let instances = vec![instance("a"), instance("b")];
171 let picked = Random.pick(&instances);
172 assert!(picked.is_some());
173 }
174
175 #[test]
176 fn weighted_non_empty() {
177 let instances = vec![instance("a"), instance("b")];
178 let picked = Weighted.pick(&instances);
179 assert!(picked.is_some());
180 }
181
182 #[test]
183 fn least_connections_prefers_idle() {
184 let lc = LeastConnections::new();
185 let instances = vec![instance("a"), instance("b")];
186
187 lc.acquire("a");
188 lc.acquire("a");
189 lc.acquire("b");
190
191 assert_eq!(lc.pick(&instances).unwrap().id, "b");
192 }
193}