Skip to main content

rust_zero_core/
balancer.rs

1use std::{
2    fmt,
3    sync::{
4        atomic::{AtomicU64, AtomicUsize, Ordering},
5        Arc, Mutex,
6    },
7    time::{Duration, Instant},
8};
9
10const DECAY_TIME: Duration = Duration::from_secs(10);
11const FORCE_PICK_AFTER: Duration = Duration::from_secs(1);
12
13struct NodeState {
14    latency_micros: f64,
15    success: f64,
16    last_update: Instant,
17    last_pick: Instant,
18}
19
20struct Node<T> {
21    value: T,
22    inflight: AtomicUsize,
23    requests: AtomicU64,
24    state: Mutex<NodeState>,
25}
26
27impl<T> Node<T> {
28    fn new(value: T) -> Self {
29        let now = Instant::now();
30        Self {
31            value,
32            inflight: AtomicUsize::new(0),
33            requests: AtomicU64::new(0),
34            state: Mutex::new(NodeState {
35                latency_micros: 0.0,
36                success: 1.0,
37                last_update: now,
38                last_pick: now,
39            }),
40        }
41    }
42
43    fn load(&self) -> f64 {
44        let state = self.state.lock().expect("P2C node state lock poisoned");
45        let latency = state.latency_micros.max(1.0).sqrt();
46        latency * (self.inflight.load(Ordering::Relaxed) as f64 + 1.0)
47    }
48
49    fn healthy(&self) -> bool {
50        self.state
51            .lock()
52            .expect("P2C node state lock poisoned")
53            .success
54            > 0.5
55    }
56
57    fn mark_picked(&self) {
58        self.state
59            .lock()
60            .expect("P2C node state lock poisoned")
61            .last_pick = Instant::now();
62        self.inflight.fetch_add(1, Ordering::Relaxed);
63        self.requests.fetch_add(1, Ordering::Relaxed);
64    }
65
66    fn complete(&self, started: Instant, success: bool) {
67        self.inflight.fetch_sub(1, Ordering::Relaxed);
68        let now = Instant::now();
69        let mut state = self.state.lock().expect("P2C node state lock poisoned");
70        let elapsed = now.saturating_duration_since(state.last_update);
71        let weight = (-elapsed.as_secs_f64() / DECAY_TIME.as_secs_f64()).exp();
72        let latency = now.saturating_duration_since(started).as_micros() as f64;
73
74        state.latency_micros = if state.latency_micros == 0.0 {
75            latency
76        } else {
77            state.latency_micros * weight + latency * (1.0 - weight)
78        };
79        let outcome = if success { 1.0 } else { 0.0 };
80        state.success = state.success * weight + outcome * (1.0 - weight);
81        state.last_update = now;
82    }
83}
84
85/// Power-of-two-choices load balancer with latency EWMA and inflight weighting.
86///
87/// Each pick returns a tracked request. Completing it feeds latency and health
88/// back into later choices; dropping it without completion records a failure.
89pub struct P2cBalancer<T> {
90    nodes: Arc<[Arc<Node<T>>]>,
91    random: AtomicU64,
92}
93
94impl<T> P2cBalancer<T> {
95    pub fn new(nodes: impl IntoIterator<Item = T>) -> Result<Self, BalancerError> {
96        let nodes: Vec<_> = nodes
97            .into_iter()
98            .map(|node| Arc::new(Node::new(node)))
99            .collect();
100        if nodes.is_empty() {
101            return Err(BalancerError::Empty);
102        }
103
104        Ok(Self {
105            nodes: nodes.into(),
106            random: AtomicU64::new(0x4d59_5df4_d0f3_3173),
107        })
108    }
109
110    pub fn len(&self) -> usize {
111        self.nodes.len()
112    }
113
114    pub fn is_empty(&self) -> bool {
115        self.nodes.is_empty()
116    }
117
118    pub fn pick(&self) -> P2cRequest<T> {
119        let selected = match self.nodes.len() {
120            1 => Arc::clone(&self.nodes[0]),
121            len => {
122                let first = self.next_index(len);
123                let mut second = self.next_index(len - 1);
124                if second >= first {
125                    second += 1;
126                }
127                choose(&self.nodes[first], &self.nodes[second])
128            }
129        };
130
131        selected.mark_picked();
132        P2cRequest {
133            node: Some(selected),
134            started: Instant::now(),
135        }
136    }
137
138    fn next_index(&self, len: usize) -> usize {
139        let mut current = self.random.load(Ordering::Relaxed);
140        loop {
141            let mut next = current;
142            next ^= next << 13;
143            next ^= next >> 7;
144            next ^= next << 17;
145            match self.random.compare_exchange_weak(
146                current,
147                next,
148                Ordering::Relaxed,
149                Ordering::Relaxed,
150            ) {
151                Ok(_) => return next as usize % len,
152                Err(actual) => current = actual,
153            }
154        }
155    }
156
157    pub fn snapshots(&self) -> Vec<NodeSnapshot<'_, T>> {
158        self.nodes
159            .iter()
160            .map(|node| {
161                let state = node.state.lock().expect("P2C node state lock poisoned");
162                NodeSnapshot {
163                    value: &node.value,
164                    inflight: node.inflight.load(Ordering::Relaxed),
165                    requests: node.requests.load(Ordering::Relaxed),
166                    latency: Duration::from_secs_f64(state.latency_micros / 1_000_000.0),
167                    success_rate: state.success,
168                }
169            })
170            .collect()
171    }
172}
173
174fn choose<T>(first: &Arc<Node<T>>, second: &Arc<Node<T>>) -> Arc<Node<T>> {
175    let first_healthy = first.healthy();
176    let second_healthy = second.healthy();
177    if first_healthy != second_healthy {
178        return Arc::clone(if first_healthy { first } else { second });
179    }
180
181    {
182        let state = second.state.lock().expect("P2C node state lock poisoned");
183        if state.last_pick.elapsed() > FORCE_PICK_AFTER {
184            return Arc::clone(second);
185        }
186    }
187
188    Arc::clone(if first.load() <= second.load() {
189        first
190    } else {
191        second
192    })
193}
194
195/// A selected node whose completion updates the balancer.
196pub struct P2cRequest<T> {
197    node: Option<Arc<Node<T>>>,
198    started: Instant,
199}
200
201impl<T> P2cRequest<T> {
202    pub fn value(&self) -> &T {
203        &self
204            .node
205            .as_ref()
206            .expect("P2C request already completed")
207            .value
208    }
209
210    pub fn complete(mut self, success: bool) {
211        if let Some(node) = self.node.take() {
212            node.complete(self.started, success);
213        }
214    }
215}
216
217impl<T> Drop for P2cRequest<T> {
218    fn drop(&mut self) {
219        if let Some(node) = self.node.take() {
220            node.complete(self.started, false);
221        }
222    }
223}
224
225#[derive(Debug)]
226pub struct NodeSnapshot<'a, T> {
227    pub value: &'a T,
228    pub inflight: usize,
229    pub requests: u64,
230    pub latency: Duration,
231    pub success_rate: f64,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum BalancerError {
236    Empty,
237}
238
239impl fmt::Display for BalancerError {
240    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241        formatter.write_str("a P2C balancer requires at least one node")
242    }
243}
244
245impl std::error::Error for BalancerError {}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn rejects_an_empty_pool() {
253        assert!(matches!(
254            P2cBalancer::<&str>::new([]),
255            Err(BalancerError::Empty)
256        ));
257    }
258
259    #[test]
260    fn tracks_inflight_requests_and_completions() {
261        let balancer = P2cBalancer::new(["one"]).unwrap();
262        let request = balancer.pick();
263        assert_eq!(request.value(), &"one");
264        assert_eq!(balancer.snapshots()[0].inflight, 1);
265
266        request.complete(true);
267        let snapshot = balancer.snapshots();
268        assert_eq!(snapshot[0].inflight, 0);
269        assert_eq!(snapshot[0].requests, 1);
270        assert!(snapshot[0].success_rate > 0.5);
271    }
272
273    #[test]
274    fn dropping_a_request_records_a_failure() {
275        let balancer = P2cBalancer::new(["one"]).unwrap();
276        drop(balancer.pick());
277
278        let snapshot = balancer.snapshots();
279        assert_eq!(snapshot[0].inflight, 0);
280        assert!(snapshot[0].success_rate < 1.0);
281    }
282}