1use async_trait::async_trait;
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19use tokio::sync::RwLock;
20use tracing::{debug, trace, warn};
21
22use zentinel_common::errors::{ZentinelError, ZentinelResult};
23
24use super::{LoadBalancer, RequestContext, TargetSelection, UpstreamTarget};
25
26#[derive(Debug, Clone)]
28pub struct PeakEwmaConfig {
29 pub decay_time: Duration,
32 pub initial_latency: Duration,
34 pub load_penalty: f64,
37}
38
39impl Default for PeakEwmaConfig {
40 fn default() -> Self {
41 Self {
42 decay_time: Duration::from_secs(10),
43 initial_latency: Duration::from_millis(1),
44 load_penalty: 1.5,
45 }
46 }
47}
48
49struct TargetStats {
51 ewma_ns: AtomicU64,
53 last_latency_ns: AtomicU64,
55 last_update_ns: AtomicU64,
57 active_connections: AtomicU64,
59 epoch: Instant,
61}
62
63impl TargetStats {
64 fn new(initial_latency: Duration) -> Self {
65 let initial_ns = initial_latency.as_nanos() as u64;
66 Self {
67 ewma_ns: AtomicU64::new(initial_ns),
68 last_latency_ns: AtomicU64::new(initial_ns),
69 last_update_ns: AtomicU64::new(0),
70 active_connections: AtomicU64::new(0),
71 epoch: Instant::now(),
72 }
73 }
74
75 fn update(&self, latency: Duration, decay_time: Duration) {
77 let latency_ns = latency.as_nanos() as u64;
78 let now_ns = self.epoch.elapsed().as_nanos() as u64;
79 let last_update = self.last_update_ns.load(Ordering::Relaxed);
80
81 let elapsed_ns = now_ns.saturating_sub(last_update);
83 let decay = (-((elapsed_ns as f64) / (decay_time.as_nanos() as f64))).exp();
84
85 let old_ewma = self.ewma_ns.load(Ordering::Relaxed);
87 let new_ewma = ((old_ewma as f64) * decay + (latency_ns as f64) * (1.0 - decay)) as u64;
88
89 self.ewma_ns.store(new_ewma, Ordering::Relaxed);
90 self.last_latency_ns.store(latency_ns, Ordering::Relaxed);
91 self.last_update_ns.store(now_ns, Ordering::Relaxed);
92 }
93
94 fn peak_latency_ns(&self) -> u64 {
96 let ewma = self.ewma_ns.load(Ordering::Relaxed);
97 let last = self.last_latency_ns.load(Ordering::Relaxed);
98 ewma.max(last)
99 }
100
101 fn load_score(&self, load_penalty: f64) -> f64 {
103 let latency = self.peak_latency_ns() as f64;
104 let active = self.active_connections.load(Ordering::Relaxed) as f64;
105 latency * (1.0 + active * load_penalty)
106 }
107
108 fn increment_connections(&self) {
109 self.active_connections.fetch_add(1, Ordering::Relaxed);
110 }
111
112 fn decrement_connections(&self) {
113 let prev = self.active_connections.fetch_sub(1, Ordering::Relaxed);
114 if prev == 0 {
115 self.active_connections.fetch_add(1, Ordering::Relaxed);
116 warn!("Attempted to decrement active connections below zero");
117 }
118 }
119}
120
121pub struct PeakEwmaBalancer {
123 targets: Vec<UpstreamTarget>,
125 stats: HashMap<String, Arc<TargetStats>>,
127 health_status: Arc<RwLock<HashMap<String, bool>>>,
129 config: PeakEwmaConfig,
131}
132
133impl PeakEwmaBalancer {
134 pub fn new(targets: Vec<UpstreamTarget>, config: PeakEwmaConfig) -> Self {
136 let mut health_status = HashMap::new();
137 let mut stats = HashMap::new();
138
139 for target in &targets {
140 let addr = target.full_address();
141 health_status.insert(addr.clone(), true);
142 stats.insert(addr, Arc::new(TargetStats::new(config.initial_latency)));
143 }
144
145 Self {
146 targets,
147 stats,
148 health_status: Arc::new(RwLock::new(health_status)),
149 config,
150 }
151 }
152}
153
154#[async_trait]
155impl LoadBalancer for PeakEwmaBalancer {
156 async fn select(&self, _context: Option<&RequestContext>) -> ZentinelResult<TargetSelection> {
157 trace!(
158 total_targets = self.targets.len(),
159 algorithm = "peak_ewma",
160 "Selecting upstream target"
161 );
162
163 let health = self.health_status.read().await;
164 let healthy_targets: Vec<_> = self
165 .targets
166 .iter()
167 .filter(|t| *health.get(&t.full_address()).unwrap_or(&true))
168 .collect();
169 drop(health);
170
171 if healthy_targets.is_empty() {
172 warn!(
173 total_targets = self.targets.len(),
174 algorithm = "peak_ewma",
175 "No healthy upstream targets available"
176 );
177 return Err(ZentinelError::NoHealthyUpstream);
178 }
179
180 let mut best_target = None;
182 let mut best_score = f64::MAX;
183
184 for target in &healthy_targets {
185 let addr = target.full_address();
186 if let Some(stats) = self.stats.get(&addr) {
187 let score = stats.load_score(self.config.load_penalty);
188 trace!(
189 target = %addr,
190 score = score,
191 ewma_ns = stats.ewma_ns.load(Ordering::Relaxed),
192 active_connections = stats.active_connections.load(Ordering::Relaxed),
193 "Evaluating target load score"
194 );
195 if score < best_score {
196 best_score = score;
197 best_target = Some(target);
198 }
199 }
200 }
201
202 let target = best_target.ok_or(ZentinelError::NoHealthyUpstream)?;
203
204 if let Some(stats) = self.stats.get(&target.full_address()) {
206 stats.increment_connections();
207 }
208
209 trace!(
210 selected_target = %target.full_address(),
211 load_score = best_score,
212 healthy_count = healthy_targets.len(),
213 algorithm = "peak_ewma",
214 "Selected target via Peak EWMA"
215 );
216
217 Ok(TargetSelection {
218 address: target.full_address(),
219 weight: target.weight,
220 metadata: HashMap::new(),
221 })
222 }
223
224 async fn release(&self, selection: &TargetSelection) {
225 if let Some(stats) = self.stats.get(&selection.address) {
226 stats.decrement_connections();
227 trace!(
228 target = %selection.address,
229 active_connections = stats.active_connections.load(Ordering::Relaxed),
230 algorithm = "peak_ewma",
231 "Released connection"
232 );
233 }
234 }
235
236 async fn report_result(
237 &self,
238 selection: &TargetSelection,
239 success: bool,
240 latency: Option<Duration>,
241 ) {
242 self.release(selection).await;
244
245 if let Some(latency) = latency {
247 if let Some(stats) = self.stats.get(&selection.address) {
248 stats.update(latency, self.config.decay_time);
249 trace!(
250 target = %selection.address,
251 latency_ms = latency.as_millis(),
252 new_ewma_ns = stats.ewma_ns.load(Ordering::Relaxed),
253 algorithm = "peak_ewma",
254 "Updated EWMA latency"
255 );
256 }
257 }
258
259 if !success {
261 self.report_health(&selection.address, false).await;
262 }
263 }
264
265 async fn report_result_with_latency(
266 &self,
267 address: &str,
268 success: bool,
269 latency: Option<Duration>,
270 ) {
271 if let Some(latency) = latency {
273 if let Some(stats) = self.stats.get(address) {
274 stats.update(latency, self.config.decay_time);
275 debug!(
276 target = %address,
277 latency_ms = latency.as_millis(),
278 new_ewma_ns = stats.ewma_ns.load(Ordering::Relaxed),
279 algorithm = "peak_ewma",
280 "Updated EWMA latency via report_result_with_latency"
281 );
282 }
283 }
284
285 self.report_health(address, success).await;
287 }
288
289 async fn report_health(&self, address: &str, healthy: bool) {
290 trace!(
291 target = %address,
292 healthy = healthy,
293 algorithm = "peak_ewma",
294 "Updating target health status"
295 );
296 self.health_status
297 .write()
298 .await
299 .insert(address.to_string(), healthy);
300 }
301
302 async fn healthy_targets(&self) -> Vec<String> {
303 self.health_status
304 .read()
305 .await
306 .iter()
307 .filter_map(|(addr, &healthy)| if healthy { Some(addr.clone()) } else { None })
308 .collect()
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 fn make_targets(count: usize) -> Vec<UpstreamTarget> {
317 (0..count)
318 .map(|i| UpstreamTarget::new(format!("backend-{}", i), 8080, 100))
319 .collect()
320 }
321
322 #[tokio::test]
323 async fn test_selects_lowest_latency() {
324 let targets = make_targets(3);
325 let balancer = PeakEwmaBalancer::new(targets, PeakEwmaConfig::default());
326
327 let addr0 = "backend-0:8080".to_string();
329 let addr1 = "backend-1:8080".to_string();
330 let addr2 = "backend-2:8080".to_string();
331
332 balancer
334 .stats
335 .get(&addr0)
336 .unwrap()
337 .update(Duration::from_millis(100), Duration::from_secs(10));
338 balancer
339 .stats
340 .get(&addr1)
341 .unwrap()
342 .update(Duration::from_millis(10), Duration::from_secs(10));
343 balancer
344 .stats
345 .get(&addr2)
346 .unwrap()
347 .update(Duration::from_millis(50), Duration::from_secs(10));
348
349 let selection = balancer.select(None).await.unwrap();
351 assert_eq!(selection.address, addr1);
352 }
353
354 #[tokio::test]
355 async fn test_considers_active_connections() {
356 let targets = make_targets(2);
357 let balancer = PeakEwmaBalancer::new(targets, PeakEwmaConfig::default());
358
359 let addr0 = "backend-0:8080".to_string();
360 let addr1 = "backend-1:8080".to_string();
361
362 balancer
364 .stats
365 .get(&addr0)
366 .unwrap()
367 .update(Duration::from_millis(10), Duration::from_secs(10));
368 balancer
369 .stats
370 .get(&addr1)
371 .unwrap()
372 .update(Duration::from_millis(10), Duration::from_secs(10));
373
374 for _ in 0..5 {
376 balancer.stats.get(&addr0).unwrap().increment_connections();
377 }
378
379 let selection = balancer.select(None).await.unwrap();
381 assert_eq!(selection.address, addr1);
382 }
383
384 #[tokio::test]
385 async fn test_ewma_decay() {
386 let targets = make_targets(1);
387 let config = PeakEwmaConfig {
388 decay_time: Duration::from_millis(100),
389 initial_latency: Duration::from_millis(50), load_penalty: 1.5,
391 };
392 let balancer = PeakEwmaBalancer::new(targets, config);
393
394 let addr = "backend-0:8080".to_string();
395 let stats = balancer.stats.get(&addr).unwrap();
396
397 tokio::time::sleep(Duration::from_millis(50)).await;
399
400 stats.update(Duration::from_millis(100), Duration::from_millis(100));
402 let after_high = stats.ewma_ns.load(Ordering::Relaxed);
403
404 tokio::time::sleep(Duration::from_millis(200)).await;
406 stats.update(Duration::from_millis(10), Duration::from_millis(100));
407 let after_low = stats.ewma_ns.load(Ordering::Relaxed);
408
409 let low_latency_ns = Duration::from_millis(10).as_nanos() as u64;
414 let high_latency_ns = Duration::from_millis(100).as_nanos() as u64;
415
416 assert!(
418 after_low < high_latency_ns,
419 "EWMA after low update ({}) should be less than high latency ({})",
420 after_low,
421 high_latency_ns
422 );
423 assert!(
424 after_low > low_latency_ns,
425 "EWMA after low update ({}) should be greater than low latency ({}) due to some carry-over",
426 after_low,
427 low_latency_ns
428 );
429 }
430
431 #[tokio::test]
432 async fn test_connection_tracking() {
433 let targets = make_targets(1);
434 let balancer = PeakEwmaBalancer::new(targets, PeakEwmaConfig::default());
435
436 let selection = balancer.select(None).await.unwrap();
438 let stats = balancer.stats.get(&selection.address).unwrap();
439 assert_eq!(stats.active_connections.load(Ordering::Relaxed), 1);
440
441 balancer.release(&selection).await;
443 assert_eq!(stats.active_connections.load(Ordering::Relaxed), 0);
444 }
445}