1use std::collections::{HashMap, VecDeque};
7
8pub type PlbPeerId = [u8; 32];
12
13#[derive(Debug, Clone, PartialEq, thiserror::Error)]
17pub enum PlbError {
18 #[error("no healthy peers available")]
20 NoHealthyPeers,
21 #[error("peer not found")]
23 PeerNotFound,
24 #[error("weight computation overflow")]
26 WeightOverflow,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum PlbStrategy {
34 #[default]
36 RoundRobin,
37 WeightedRandom,
39 LeastConnections,
41 LeastLatency,
43 ConsistentHash,
45 PowerOfTwo,
47}
48
49#[derive(Debug, Clone)]
53pub struct PlbBalancerConfig {
54 pub strategy: PlbStrategy,
56 pub health_threshold: f64,
59 pub max_in_flight: u32,
61 pub cooldown_ms: u64,
63 pub virtual_nodes: u32,
65 pub ewma_alpha: f64,
67}
68
69impl Default for PlbBalancerConfig {
70 fn default() -> Self {
71 Self {
72 strategy: PlbStrategy::default(),
73 health_threshold: 0.8,
74 max_in_flight: 64,
75 cooldown_ms: 5_000,
76 virtual_nodes: 150,
77 ewma_alpha: 0.1,
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
86pub struct PlbPeerState {
87 pub id: PlbPeerId,
89 pub capacity: u32,
91 pub in_flight: u32,
93 pub success_count: u64,
95 pub failure_count: u64,
97 pub avg_latency_ms: f64,
99 pub last_used_ts: u64,
101 pub is_healthy: bool,
103 pub weight: f64,
105 pub cooldown_until_ts: u64,
107}
108
109impl PlbPeerState {
110 fn new(id: PlbPeerId, capacity: u32) -> Self {
111 Self {
112 id,
113 capacity,
114 in_flight: 0,
115 success_count: 0,
116 failure_count: 0,
117 avg_latency_ms: 0.0,
118 last_used_ts: 0,
119 is_healthy: true,
120 weight: 1.0,
121 cooldown_until_ts: 0,
122 }
123 }
124
125 fn is_available(&self, max_in_flight: u32, now_ms: u64) -> bool {
127 self.is_healthy
128 && self.in_flight < max_in_flight
129 && self.in_flight < self.capacity
130 && (self.cooldown_until_ts == 0 || now_ms >= self.cooldown_until_ts)
131 }
132
133 pub fn failure_rate(&self) -> f64 {
135 let total = self.success_count + self.failure_count;
136 if total == 0 {
137 0.0
138 } else {
139 self.failure_count as f64 / total as f64
140 }
141 }
142}
143
144#[derive(Debug, Clone)]
148pub struct PlbRequestRecord {
149 pub ts: u64,
151 pub peer_id: PlbPeerId,
153 pub latency_ms: f64,
155 pub success: bool,
157}
158
159#[derive(Debug, Clone)]
163pub struct PlbPeerStats {
164 pub id: PlbPeerId,
166 pub in_flight: u32,
168 pub success_count: u64,
170 pub failure_count: u64,
172 pub avg_latency_ms: f64,
174 pub weight: f64,
176 pub is_healthy: bool,
178}
179
180#[derive(Debug, Clone)]
182pub struct PlbBalancerStats {
183 pub total_requests: u64,
185 pub error_rate: f64,
187 pub avg_latency_ms: f64,
189 pub peer_stats: Vec<PlbPeerStats>,
191 pub healthy_peers: usize,
193 pub cooling_peers: usize,
195}
196
197#[inline]
201fn xorshift64(state: &mut u64) -> u64 {
202 let mut x = *state;
203 x ^= x << 13;
204 x ^= x >> 7;
205 x ^= x << 17;
206 *state = x;
207 x
208}
209
210#[inline]
212fn fnv1a_64(data: &[u8]) -> u64 {
213 let mut h: u64 = 14_695_981_039_346_656_037;
214 for &b in data {
215 h ^= b as u64;
216 h = h.wrapping_mul(1_099_511_628_211);
217 }
218 h
219}
220
221#[derive(Debug, Clone)]
225struct RingEntry {
226 hash: u64,
227 peer_id: PlbPeerId,
228}
229
230pub struct PeerLoadBalancer {
238 peers: HashMap<PlbPeerId, PlbPeerState>,
239 request_log: VecDeque<PlbRequestRecord>,
240 config: PlbBalancerConfig,
241 rr_cursor: usize,
243 prng_state: u64,
245 total_requests: u64,
247 ring: Vec<RingEntry>,
249 ring_dirty: bool,
251}
252
253impl PeerLoadBalancer {
254 const LOG_CAPACITY: usize = 2_000;
256
257 pub fn new(config: PlbBalancerConfig) -> Self {
259 Self {
260 peers: HashMap::new(),
261 request_log: VecDeque::with_capacity(Self::LOG_CAPACITY),
262 config,
263 rr_cursor: 0,
264 prng_state: 0xDEAD_BEEF_CAFE_1234,
265 total_requests: 0,
266 ring: Vec::new(),
267 ring_dirty: false,
268 }
269 }
270
271 pub fn default_config() -> Self {
273 Self::new(PlbBalancerConfig::default())
274 }
275
276 pub fn add_peer(&mut self, id: PlbPeerId, capacity: u32) {
281 self.peers
282 .entry(id)
283 .and_modify(|s| s.capacity = capacity)
284 .or_insert_with(|| PlbPeerState::new(id, capacity));
285 self.ring_dirty = true;
286 self.recompute_weights();
287 }
288
289 pub fn remove_peer(&mut self, id: &PlbPeerId) -> Option<PlbPeerState> {
291 let removed = self.peers.remove(id);
292 if removed.is_some() {
293 self.ring_dirty = true;
294 self.recompute_weights();
295 }
296 removed
297 }
298
299 pub fn set_healthy(&mut self, id: &PlbPeerId, healthy: bool) -> Result<(), PlbError> {
301 let peer = self.peers.get_mut(id).ok_or(PlbError::PeerNotFound)?;
302 peer.is_healthy = healthy;
303 self.recompute_weights();
304 Ok(())
305 }
306
307 pub fn cooldown_peer(&mut self, id: &PlbPeerId) -> Result<(), PlbError> {
309 let now_ms = now_ms();
310 let cooldown = self.config.cooldown_ms;
311 let peer = self.peers.get_mut(id).ok_or(PlbError::PeerNotFound)?;
312 peer.cooldown_until_ts = now_ms.saturating_add(cooldown);
313 Ok(())
314 }
315
316 pub fn select_peer(&mut self, key: &[u8]) -> Result<PlbPeerId, PlbError> {
322 let now = now_ms();
323 let strategy = self.config.strategy;
324 let selected = match strategy {
325 PlbStrategy::RoundRobin => self.select_round_robin(now)?,
326 PlbStrategy::WeightedRandom => self.select_weighted_random(now)?,
327 PlbStrategy::LeastConnections => self.select_least_connections(now)?,
328 PlbStrategy::LeastLatency => self.select_least_latency(now)?,
329 PlbStrategy::ConsistentHash => self.select_consistent_hash(key, now)?,
330 PlbStrategy::PowerOfTwo => self.select_power_of_two(now)?,
331 };
332 if let Some(peer) = self.peers.get_mut(&selected) {
333 peer.in_flight = peer.in_flight.saturating_add(1);
334 peer.last_used_ts = now;
335 }
336 self.total_requests = self.total_requests.saturating_add(1);
337 Ok(selected)
338 }
339
340 pub fn record_result(
346 &mut self,
347 peer_id: &PlbPeerId,
348 latency_ms: f64,
349 success: bool,
350 ) -> Result<(), PlbError> {
351 let alpha = self.config.ewma_alpha;
352 {
353 let peer = self.peers.get_mut(peer_id).ok_or(PlbError::PeerNotFound)?;
354 peer.in_flight = peer.in_flight.saturating_sub(1);
356 if peer.avg_latency_ms == 0.0 {
358 peer.avg_latency_ms = latency_ms;
359 } else {
360 peer.avg_latency_ms = alpha * latency_ms + (1.0 - alpha) * peer.avg_latency_ms;
361 }
362 if success {
363 peer.success_count = peer.success_count.saturating_add(1);
364 } else {
365 peer.failure_count = peer.failure_count.saturating_add(1);
366 }
367 }
368 let record = PlbRequestRecord {
370 ts: now_ms(),
371 peer_id: *peer_id,
372 latency_ms,
373 success,
374 };
375 if self.request_log.len() >= Self::LOG_CAPACITY {
376 self.request_log.pop_front();
377 }
378 self.request_log.push_back(record);
379 self.recompute_weights();
380 Ok(())
381 }
382
383 pub fn recompute_weights(&mut self) {
395 let ids: Vec<PlbPeerId> = self.peers.keys().copied().collect();
397 let mut raw: Vec<(PlbPeerId, f64)> = ids
398 .into_iter()
399 .map(|id| {
400 let p = &self.peers[&id];
401 let capacity_score = if p.capacity == 0 {
402 0.0
403 } else {
404 let avail = p.capacity.saturating_sub(p.in_flight) as f64;
405 (avail / p.capacity as f64).clamp(0.0, 1.0)
406 };
407 let latency_score = 1.0 / (1.0 + p.avg_latency_ms / 1000.0);
408 let total = p.success_count + p.failure_count;
409 let health_score = if total == 0 {
410 1.0
411 } else {
412 p.success_count as f64 / total as f64
413 };
414 let w = capacity_score * latency_score * health_score;
415 (id, w.max(0.0))
416 })
417 .collect();
418
419 let sum: f64 = raw.iter().map(|(_, w)| *w).sum();
421 for (id, w) in &mut raw {
422 let normalised = if sum > 0.0 { *w / sum } else { 0.0 };
423 if let Some(peer) = self.peers.get_mut(id) {
424 peer.weight = normalised;
425 }
426 }
427 }
428
429 pub fn run_health_check(&mut self) {
434 let threshold = self.config.health_threshold;
435 let ids: Vec<PlbPeerId> = self.peers.keys().copied().collect();
436 for id in ids {
437 if let Some(peer) = self.peers.get_mut(&id) {
438 let total = peer.success_count + peer.failure_count;
439 if total > 0 {
440 let failure_rate = peer.failure_count as f64 / total as f64;
441 peer.is_healthy = failure_rate <= 1.0 - threshold;
443 }
444 }
445 }
446 self.recompute_weights();
447 }
448
449 pub fn balancer_stats(&self) -> PlbBalancerStats {
453 let now = now_ms();
454 let total = self.request_log.len() as u64;
455 let errors = self.request_log.iter().filter(|r| !r.success).count() as u64;
456 let error_rate = if total == 0 {
457 0.0
458 } else {
459 errors as f64 / total as f64
460 };
461 let avg_latency_ms = if total == 0 {
462 0.0
463 } else {
464 self.request_log.iter().map(|r| r.latency_ms).sum::<f64>() / total as f64
465 };
466 let peer_stats: Vec<PlbPeerStats> = self
467 .peers
468 .values()
469 .map(|p| PlbPeerStats {
470 id: p.id,
471 in_flight: p.in_flight,
472 success_count: p.success_count,
473 failure_count: p.failure_count,
474 avg_latency_ms: p.avg_latency_ms,
475 weight: p.weight,
476 is_healthy: p.is_healthy,
477 })
478 .collect();
479 let healthy_peers = self
480 .peers
481 .values()
482 .filter(|p| p.is_available(self.config.max_in_flight, now))
483 .count();
484 let cooling_peers = self
485 .peers
486 .values()
487 .filter(|p| p.cooldown_until_ts > 0 && now < p.cooldown_until_ts)
488 .count();
489 PlbBalancerStats {
490 total_requests: self.total_requests,
491 error_rate,
492 avg_latency_ms,
493 peer_stats,
494 healthy_peers,
495 cooling_peers,
496 }
497 }
498
499 pub fn config(&self) -> &PlbBalancerConfig {
501 &self.config
502 }
503
504 pub fn peer_count(&self) -> usize {
506 self.peers.len()
507 }
508
509 pub fn peer_state(&self, id: &PlbPeerId) -> Option<&PlbPeerState> {
511 self.peers.get(id)
512 }
513
514 fn available_peers(&self, now: u64) -> Vec<PlbPeerId> {
517 self.peers
518 .values()
519 .filter(|p| p.is_available(self.config.max_in_flight, now))
520 .map(|p| p.id)
521 .collect()
522 }
523
524 fn select_round_robin(&mut self, now: u64) -> Result<PlbPeerId, PlbError> {
525 let mut available = self.available_peers(now);
526 if available.is_empty() {
527 return Err(PlbError::NoHealthyPeers);
528 }
529 available.sort_unstable();
531 let idx = self.rr_cursor % available.len();
532 self.rr_cursor = self.rr_cursor.wrapping_add(1);
533 Ok(available[idx])
534 }
535
536 fn select_weighted_random(&mut self, now: u64) -> Result<PlbPeerId, PlbError> {
537 let available: Vec<&PlbPeerState> = self
538 .peers
539 .values()
540 .filter(|p| p.is_available(self.config.max_in_flight, now))
541 .collect();
542 if available.is_empty() {
543 return Err(PlbError::NoHealthyPeers);
544 }
545 let total_weight: f64 = available.iter().map(|p| p.weight).sum();
546 if total_weight <= 0.0 {
547 let r = xorshift64(&mut self.prng_state) as usize % available.len();
549 return Ok(available[r].id);
550 }
551 let r = (xorshift64(&mut self.prng_state) as f64 / u64::MAX as f64) * total_weight;
552 let mut cumulative = 0.0;
553 for p in &available {
554 cumulative += p.weight;
555 if r <= cumulative {
556 return Ok(p.id);
557 }
558 }
559 Ok(available[available.len() - 1].id)
561 }
562
563 fn select_least_connections(&self, now: u64) -> Result<PlbPeerId, PlbError> {
564 self.peers
565 .values()
566 .filter(|p| p.is_available(self.config.max_in_flight, now))
567 .min_by_key(|p| p.in_flight)
568 .map(|p| p.id)
569 .ok_or(PlbError::NoHealthyPeers)
570 }
571
572 fn select_least_latency(&self, now: u64) -> Result<PlbPeerId, PlbError> {
573 self.peers
574 .values()
575 .filter(|p| p.is_available(self.config.max_in_flight, now))
576 .min_by(|a, b| {
577 a.avg_latency_ms
578 .partial_cmp(&b.avg_latency_ms)
579 .unwrap_or(std::cmp::Ordering::Equal)
580 })
581 .map(|p| p.id)
582 .ok_or(PlbError::NoHealthyPeers)
583 }
584
585 fn select_consistent_hash(&mut self, key: &[u8], now: u64) -> Result<PlbPeerId, PlbError> {
586 if self.ring_dirty {
588 self.rebuild_ring();
589 }
590 if self.ring.is_empty() {
591 return Err(PlbError::NoHealthyPeers);
592 }
593 let h = fnv1a_64(key);
594 let pos = self.ring.partition_point(|entry| entry.hash < h);
596 let len = self.ring.len();
598 for offset in 0..len {
599 let idx = (pos + offset) % len;
600 let candidate = self.ring[idx].peer_id;
601 if let Some(peer) = self.peers.get(&candidate) {
602 if peer.is_available(self.config.max_in_flight, now) {
603 return Ok(candidate);
604 }
605 }
606 }
607 Err(PlbError::NoHealthyPeers)
608 }
609
610 fn select_power_of_two(&mut self, now: u64) -> Result<PlbPeerId, PlbError> {
611 let available: Vec<PlbPeerId> = self.available_peers(now);
612 if available.is_empty() {
613 return Err(PlbError::NoHealthyPeers);
614 }
615 if available.len() == 1 {
616 return Ok(available[0]);
617 }
618 let i1 = (xorshift64(&mut self.prng_state) as usize) % available.len();
620 let mut i2 = (xorshift64(&mut self.prng_state) as usize) % available.len();
621 if i2 == i1 {
622 i2 = (i1 + 1) % available.len();
623 }
624 let p1 = available[i1];
625 let p2 = available[i2];
626 let s1 = self.peers.get(&p1);
628 let s2 = self.peers.get(&p2);
629 match (s1, s2) {
630 (Some(a), Some(b)) => {
631 if a.in_flight < b.in_flight {
632 Ok(p1)
633 } else if b.in_flight < a.in_flight {
634 Ok(p2)
635 } else if a.avg_latency_ms <= b.avg_latency_ms {
636 Ok(p1)
637 } else {
638 Ok(p2)
639 }
640 }
641 (Some(_), None) => Ok(p1),
642 (None, Some(_)) => Ok(p2),
643 (None, None) => Err(PlbError::NoHealthyPeers),
644 }
645 }
646
647 fn rebuild_ring(&mut self) {
648 let virtual_nodes = self.config.virtual_nodes;
649 let mut ring: Vec<RingEntry> = self
650 .peers
651 .values()
652 .flat_map(|p| {
653 (0..virtual_nodes).map(move |i| {
654 let mut vnode_key = Vec::with_capacity(32 + 4);
655 vnode_key.extend_from_slice(&p.id);
656 vnode_key.extend_from_slice(&i.to_le_bytes());
657 RingEntry {
658 hash: fnv1a_64(&vnode_key),
659 peer_id: p.id,
660 }
661 })
662 })
663 .collect();
664 ring.sort_unstable_by_key(|e| e.hash);
665 self.ring = ring;
666 self.ring_dirty = false;
667 }
668}
669
670fn now_ms() -> u64 {
673 use std::time::{SystemTime, UNIX_EPOCH};
674 SystemTime::now()
675 .duration_since(UNIX_EPOCH)
676 .map(|d| d.as_millis() as u64)
677 .unwrap_or(0)
678}
679
680pub type PlbPeerLoadBalancer = PeerLoadBalancer;
684
685#[cfg(test)]
688mod tests {
689 use super::*;
690
691 fn peer_id(b: u8) -> PlbPeerId {
693 let mut id = [0u8; 32];
694 id[0] = b;
695 id
696 }
697
698 fn balancer(strategy: PlbStrategy) -> PeerLoadBalancer {
700 PeerLoadBalancer::new(PlbBalancerConfig {
701 strategy,
702 ..Default::default()
703 })
704 }
705
706 #[test]
709 fn test_new_balancer_has_no_peers() {
710 let lb = PeerLoadBalancer::default_config();
711 assert_eq!(lb.peer_count(), 0);
712 }
713
714 #[test]
715 fn test_config_is_stored() {
716 let cfg = PlbBalancerConfig {
717 strategy: PlbStrategy::LeastConnections,
718 health_threshold: 0.9,
719 max_in_flight: 16,
720 cooldown_ms: 1000,
721 virtual_nodes: 50,
722 ewma_alpha: 0.2,
723 };
724 let lb = PeerLoadBalancer::new(cfg.clone());
725 assert_eq!(lb.config().max_in_flight, 16);
726 assert_eq!(lb.config().cooldown_ms, 1000);
727 }
728
729 #[test]
730 fn test_default_strategy_is_round_robin() {
731 let lb = PeerLoadBalancer::default_config();
732 assert_eq!(lb.config().strategy, PlbStrategy::RoundRobin);
733 }
734
735 #[test]
738 fn test_add_peer_increments_count() {
739 let mut lb = PeerLoadBalancer::default_config();
740 lb.add_peer(peer_id(1), 10);
741 assert_eq!(lb.peer_count(), 1);
742 }
743
744 #[test]
745 fn test_add_multiple_peers() {
746 let mut lb = PeerLoadBalancer::default_config();
747 lb.add_peer(peer_id(1), 10);
748 lb.add_peer(peer_id(2), 20);
749 lb.add_peer(peer_id(3), 5);
750 assert_eq!(lb.peer_count(), 3);
751 }
752
753 #[test]
754 fn test_add_duplicate_peer_updates_capacity() {
755 let mut lb = PeerLoadBalancer::default_config();
756 lb.add_peer(peer_id(1), 10);
757 lb.add_peer(peer_id(1), 50);
758 assert_eq!(lb.peer_count(), 1);
759 assert_eq!(
760 lb.peer_state(&peer_id(1))
761 .expect("test: peer 1 should exist")
762 .capacity,
763 50
764 );
765 }
766
767 #[test]
768 fn test_remove_peer_decrements_count() {
769 let mut lb = PeerLoadBalancer::default_config();
770 lb.add_peer(peer_id(1), 10);
771 lb.remove_peer(&peer_id(1));
772 assert_eq!(lb.peer_count(), 0);
773 }
774
775 #[test]
776 fn test_remove_nonexistent_peer_returns_none() {
777 let mut lb = PeerLoadBalancer::default_config();
778 assert!(lb.remove_peer(&peer_id(99)).is_none());
779 }
780
781 #[test]
782 fn test_peer_state_accessible() {
783 let mut lb = PeerLoadBalancer::default_config();
784 lb.add_peer(peer_id(7), 32);
785 let s = lb
786 .peer_state(&peer_id(7))
787 .expect("test: peer 7 should exist");
788 assert_eq!(s.capacity, 32);
789 assert!(s.is_healthy);
790 }
791
792 #[test]
795 fn test_set_healthy_false() {
796 let mut lb = PeerLoadBalancer::default_config();
797 lb.add_peer(peer_id(1), 10);
798 lb.set_healthy(&peer_id(1), false)
799 .expect("test: set_healthy should succeed");
800 assert!(
801 !lb.peer_state(&peer_id(1))
802 .expect("test: peer 1 should exist after set_healthy")
803 .is_healthy
804 );
805 }
806
807 #[test]
808 fn test_set_healthy_true() {
809 let mut lb = PeerLoadBalancer::default_config();
810 lb.add_peer(peer_id(1), 10);
811 lb.set_healthy(&peer_id(1), false)
812 .expect("test: set_healthy false should succeed");
813 lb.set_healthy(&peer_id(1), true)
814 .expect("test: set_healthy true should succeed");
815 assert!(
816 lb.peer_state(&peer_id(1))
817 .expect("test: peer 1 should exist after set_healthy true")
818 .is_healthy
819 );
820 }
821
822 #[test]
823 fn test_set_healthy_nonexistent_returns_error() {
824 let mut lb = PeerLoadBalancer::default_config();
825 assert_eq!(
826 lb.set_healthy(&peer_id(99), true),
827 Err(PlbError::PeerNotFound)
828 );
829 }
830
831 #[test]
832 fn test_unhealthy_peer_not_selected_round_robin() {
833 let mut lb = balancer(PlbStrategy::RoundRobin);
834 lb.add_peer(peer_id(1), 10);
835 lb.set_healthy(&peer_id(1), false)
836 .expect("test: set_healthy false should succeed");
837 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
838 }
839
840 #[test]
843 fn test_select_no_peers_returns_error() {
844 let mut lb = PeerLoadBalancer::default_config();
845 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
846 }
847
848 #[test]
851 fn test_round_robin_cycles_through_all_peers() {
852 let mut lb = balancer(PlbStrategy::RoundRobin);
853 lb.add_peer(peer_id(1), 10);
854 lb.add_peer(peer_id(2), 10);
855 lb.add_peer(peer_id(3), 10);
856 let mut seen = std::collections::HashSet::new();
857 for _ in 0..9 {
858 let p = lb
859 .select_peer(&[])
860 .expect("test: select_peer should return a peer in round-robin");
861 seen.insert(p);
862 lb.record_result(&p, 10.0, true)
864 .expect("test: record_result should succeed");
865 }
866 assert_eq!(seen.len(), 3);
867 }
868
869 #[test]
870 fn test_round_robin_single_peer() {
871 let mut lb = balancer(PlbStrategy::RoundRobin);
872 lb.add_peer(peer_id(1), 100);
873 for _ in 0..5 {
874 let p = lb
875 .select_peer(&[])
876 .expect("test: select_peer should succeed for single peer");
877 lb.record_result(&p, 5.0, true)
878 .expect("test: record_result for single peer should succeed");
879 assert_eq!(p, peer_id(1));
880 }
881 }
882
883 #[test]
886 fn test_least_connections_prefers_idle_peer() {
887 let mut lb = balancer(PlbStrategy::LeastConnections);
888 lb.add_peer(peer_id(1), 10);
889 lb.add_peer(peer_id(2), 10);
890 lb.peers
892 .get_mut(&peer_id(1))
893 .expect("test: peer 1 should exist for in_flight mutation")
894 .in_flight = 5;
895 let selected = lb
896 .select_peer(&[])
897 .expect("test: select_peer should pick least-loaded peer");
898 assert_eq!(selected, peer_id(2));
899 }
900
901 #[test]
902 fn test_least_connections_all_tied() {
903 let mut lb = balancer(PlbStrategy::LeastConnections);
904 lb.add_peer(peer_id(1), 10);
905 lb.add_peer(peer_id(2), 10);
906 assert!(lb.select_peer(&[]).is_ok());
908 }
909
910 #[test]
913 fn test_least_latency_selects_low_latency_peer() {
914 let mut lb = balancer(PlbStrategy::LeastLatency);
915 lb.add_peer(peer_id(1), 10);
916 lb.add_peer(peer_id(2), 10);
917 lb.peers
918 .get_mut(&peer_id(1))
919 .expect("test: peer 1 should exist for latency mutation")
920 .avg_latency_ms = 100.0;
921 lb.peers
922 .get_mut(&peer_id(2))
923 .expect("test: peer 2 should exist for latency mutation")
924 .avg_latency_ms = 5.0;
925 let selected = lb
926 .select_peer(&[])
927 .expect("test: select_peer should pick least-latency peer");
928 assert_eq!(selected, peer_id(2));
929 }
930
931 #[test]
932 fn test_least_latency_new_peers_have_zero_latency() {
933 let mut lb = balancer(PlbStrategy::LeastLatency);
934 lb.add_peer(peer_id(1), 10);
935 lb.add_peer(peer_id(2), 10);
936 assert!(lb.select_peer(&[]).is_ok());
938 }
939
940 #[test]
943 fn test_weighted_random_selects_from_available() {
944 let mut lb = balancer(PlbStrategy::WeightedRandom);
945 lb.add_peer(peer_id(1), 10);
946 lb.add_peer(peer_id(2), 10);
947 for _ in 0..20 {
948 let p = lb
949 .select_peer(&[])
950 .expect("test: weighted random select should succeed");
951 lb.record_result(&p, 10.0, true)
952 .expect("test: record_result in weighted random should succeed");
953 }
954 }
955
956 #[test]
957 fn test_weighted_random_single_peer_always_selected() {
958 let mut lb = balancer(PlbStrategy::WeightedRandom);
959 lb.add_peer(peer_id(5), 100);
960 for _ in 0..10 {
961 let p = lb
962 .select_peer(&[])
963 .expect("test: weighted random single peer should select it");
964 lb.record_result(&p, 10.0, true)
965 .expect("test: record_result for single peer in weighted random should succeed");
966 assert_eq!(p, peer_id(5));
967 }
968 }
969
970 #[test]
973 fn test_consistent_hash_same_key_same_peer() {
974 let mut lb = balancer(PlbStrategy::ConsistentHash);
975 lb.add_peer(peer_id(1), 100);
976 lb.add_peer(peer_id(2), 100);
977 lb.add_peer(peer_id(3), 100);
978 let key = b"my-content-key";
979 let p1 = lb
980 .select_peer(key)
981 .expect("test: consistent hash first select should succeed");
982 lb.record_result(&p1, 5.0, true)
983 .expect("test: record_result after first consistent hash select");
984 let p2 = lb
985 .select_peer(key)
986 .expect("test: consistent hash second select should succeed");
987 lb.record_result(&p2, 5.0, true)
988 .expect("test: record_result after second consistent hash select");
989 assert_eq!(p1, p2);
990 }
991
992 #[test]
993 fn test_consistent_hash_different_keys_may_differ() {
994 let mut lb = balancer(PlbStrategy::ConsistentHash);
995 lb.add_peer(peer_id(1), 100);
996 lb.add_peer(peer_id(2), 100);
997 lb.add_peer(peer_id(3), 100);
998 assert!(lb.select_peer(b"key-a").is_ok());
1000 assert!(lb.select_peer(b"key-b").is_ok());
1001 }
1002
1003 #[test]
1004 fn test_consistent_hash_empty_key() {
1005 let mut lb = balancer(PlbStrategy::ConsistentHash);
1006 lb.add_peer(peer_id(1), 10);
1007 assert!(lb.select_peer(&[]).is_ok());
1008 }
1009
1010 #[test]
1011 fn test_consistent_hash_ring_rebuilt_on_add() {
1012 let mut lb = balancer(PlbStrategy::ConsistentHash);
1013 lb.add_peer(peer_id(1), 10);
1014 let _ = lb.select_peer(b"x"); lb.add_peer(peer_id(2), 10); assert!(lb.ring_dirty);
1017 let _ = lb.select_peer(b"x"); assert!(!lb.ring_dirty);
1019 }
1020
1021 #[test]
1024 fn test_power_of_two_returns_peer() {
1025 let mut lb = balancer(PlbStrategy::PowerOfTwo);
1026 lb.add_peer(peer_id(1), 10);
1027 lb.add_peer(peer_id(2), 10);
1028 assert!(lb.select_peer(&[]).is_ok());
1029 }
1030
1031 #[test]
1032 fn test_power_of_two_single_peer() {
1033 let mut lb = balancer(PlbStrategy::PowerOfTwo);
1034 lb.add_peer(peer_id(1), 100);
1035 let p = lb
1036 .select_peer(&[])
1037 .expect("test: power-of-two single peer should be selected");
1038 assert_eq!(p, peer_id(1));
1039 }
1040
1041 #[test]
1042 fn test_power_of_two_prefers_less_loaded() {
1043 let mut lb = balancer(PlbStrategy::PowerOfTwo);
1044 lb.add_peer(peer_id(1), 100);
1045 lb.add_peer(peer_id(2), 100);
1046 for _ in 0..50 {
1049 let p = lb
1050 .select_peer(&[])
1051 .expect("test: power-of-two should select a peer");
1052 lb.record_result(&p, 10.0, true)
1053 .expect("test: record_result in power-of-two should succeed");
1054 }
1055 }
1056
1057 #[test]
1060 fn test_record_result_updates_success_count() {
1061 let mut lb = PeerLoadBalancer::default_config();
1062 lb.add_peer(peer_id(1), 10);
1063 let p = lb
1064 .select_peer(&[])
1065 .expect("test: select_peer for record_result success count test");
1066 lb.record_result(&p, 20.0, true)
1067 .expect("test: record_result success should succeed");
1068 assert_eq!(
1069 lb.peer_state(&peer_id(1))
1070 .expect("test: peer 1 should exist after success")
1071 .success_count,
1072 1
1073 );
1074 }
1075
1076 #[test]
1077 fn test_record_result_updates_failure_count() {
1078 let mut lb = PeerLoadBalancer::default_config();
1079 lb.add_peer(peer_id(1), 10);
1080 let p = lb
1081 .select_peer(&[])
1082 .expect("test: select_peer for record_result failure count test");
1083 lb.record_result(&p, 20.0, false)
1084 .expect("test: record_result failure should succeed");
1085 assert_eq!(
1086 lb.peer_state(&peer_id(1))
1087 .expect("test: peer 1 should exist after failure")
1088 .failure_count,
1089 1
1090 );
1091 }
1092
1093 #[test]
1094 fn test_record_result_decrements_in_flight() {
1095 let mut lb = PeerLoadBalancer::default_config();
1096 lb.add_peer(peer_id(1), 10);
1097 let p = lb
1098 .select_peer(&[])
1099 .expect("test: select_peer for in_flight decrement test");
1100 assert_eq!(
1101 lb.peer_state(&peer_id(1))
1102 .expect("test: peer 1 should exist to check in_flight")
1103 .in_flight,
1104 1
1105 );
1106 lb.record_result(&p, 5.0, true)
1107 .expect("test: record_result for in_flight decrement should succeed");
1108 assert_eq!(
1109 lb.peer_state(&peer_id(1))
1110 .expect("test: peer 1 should exist after record to check in_flight zero")
1111 .in_flight,
1112 0
1113 );
1114 }
1115
1116 #[test]
1117 fn test_record_result_updates_ewma_latency() {
1118 let mut lb = PeerLoadBalancer::default_config();
1119 lb.add_peer(peer_id(1), 10);
1120 let p = lb
1121 .select_peer(&[])
1122 .expect("test: select_peer for EWMA latency update test");
1123 lb.record_result(&p, 100.0, true)
1124 .expect("test: record_result for EWMA should succeed");
1125 let lat = lb
1126 .peer_state(&peer_id(1))
1127 .expect("test: peer 1 should exist to check EWMA latency")
1128 .avg_latency_ms;
1129 assert!((lat - 100.0).abs() < 0.001);
1130 }
1131
1132 #[test]
1133 fn test_record_result_ewma_converges() {
1134 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1135 ewma_alpha: 0.5,
1136 max_in_flight: 1000,
1137 ..Default::default()
1138 });
1139 lb.add_peer(peer_id(1), 1000);
1140 for _ in 0..20 {
1141 let p = lb
1142 .select_peer(&[])
1143 .expect("test: select_peer in EWMA convergence test");
1144 lb.record_result(&p, 50.0, true)
1145 .expect("test: record_result in EWMA convergence should succeed");
1146 }
1147 let lat = lb
1148 .peer_state(&peer_id(1))
1149 .expect("test: peer 1 should exist to check converged EWMA")
1150 .avg_latency_ms;
1151 assert!((lat - 50.0).abs() < 1.0);
1152 }
1153
1154 #[test]
1155 fn test_record_result_for_unknown_peer_returns_error() {
1156 let mut lb = PeerLoadBalancer::default_config();
1157 assert_eq!(
1158 lb.record_result(&peer_id(99), 10.0, true),
1159 Err(PlbError::PeerNotFound)
1160 );
1161 }
1162
1163 #[test]
1164 fn test_request_log_bounded_at_2000() {
1165 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1166 max_in_flight: 10_000,
1167 ..Default::default()
1168 });
1169 lb.add_peer(peer_id(1), 10_000);
1170 for _ in 0..2_100 {
1171 let p = lb
1172 .select_peer(&[])
1173 .expect("test: select_peer for log bound test");
1174 lb.record_result(&p, 1.0, true)
1175 .expect("test: record_result for log bound test should succeed");
1176 }
1177 assert!(lb.request_log.len() <= PeerLoadBalancer::LOG_CAPACITY);
1178 }
1179
1180 #[test]
1183 fn test_weights_sum_to_one() {
1184 let mut lb = PeerLoadBalancer::default_config();
1185 lb.add_peer(peer_id(1), 10);
1186 lb.add_peer(peer_id(2), 20);
1187 lb.add_peer(peer_id(3), 5);
1188 lb.recompute_weights();
1189 let sum: f64 = lb.peers.values().map(|p| p.weight).sum();
1190 assert!((sum - 1.0).abs() < 1e-9 || sum == 0.0);
1191 }
1192
1193 #[test]
1194 fn test_zero_capacity_peer_gets_zero_weight() {
1195 let mut lb = PeerLoadBalancer::default_config();
1196 lb.add_peer(peer_id(1), 0);
1197 lb.add_peer(peer_id(2), 10);
1198 lb.recompute_weights();
1199 assert_eq!(
1200 lb.peer_state(&peer_id(1))
1201 .expect("test: peer 1 should exist to check zero weight")
1202 .weight,
1203 0.0
1204 );
1205 }
1206
1207 #[test]
1208 fn test_unhealthy_peer_may_have_weight() {
1209 let mut lb = PeerLoadBalancer::default_config();
1213 lb.add_peer(peer_id(1), 10);
1214 lb.set_healthy(&peer_id(1), false)
1215 .expect("test: set_healthy false should succeed");
1216 lb.recompute_weights();
1217 assert!(
1219 lb.peer_state(&peer_id(1))
1220 .expect("test: peer 1 should exist to check weight >= 0")
1221 .weight
1222 >= 0.0
1223 );
1224 }
1225
1226 #[test]
1229 fn test_health_check_marks_bad_peer_unhealthy() {
1230 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1231 health_threshold: 0.8,
1232 max_in_flight: 1000,
1233 ..Default::default()
1234 });
1235 lb.add_peer(peer_id(1), 100);
1236 lb.peers
1237 .get_mut(&peer_id(1))
1238 .expect("test: peer 1 should exist for success_count mutation")
1239 .success_count = 1;
1240 lb.peers
1241 .get_mut(&peer_id(1))
1242 .expect("test: peer 1 should exist for failure_count mutation")
1243 .failure_count = 9;
1244 lb.run_health_check();
1245 assert!(
1246 !lb.peer_state(&peer_id(1))
1247 .expect("test: peer 1 should exist after health check unhealthy")
1248 .is_healthy
1249 );
1250 }
1251
1252 #[test]
1253 fn test_health_check_keeps_good_peer_healthy() {
1254 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1255 health_threshold: 0.8,
1256 max_in_flight: 1000,
1257 ..Default::default()
1258 });
1259 lb.add_peer(peer_id(1), 100);
1260 lb.peers
1261 .get_mut(&peer_id(1))
1262 .expect("test: peer 1 should exist for success_count good-peer mutation")
1263 .success_count = 9;
1264 lb.peers
1265 .get_mut(&peer_id(1))
1266 .expect("test: peer 1 should exist for failure_count good-peer mutation")
1267 .failure_count = 1;
1268 lb.run_health_check();
1269 assert!(
1270 lb.peer_state(&peer_id(1))
1271 .expect("test: peer 1 should remain healthy after good-peer health check")
1272 .is_healthy
1273 );
1274 }
1275
1276 #[test]
1277 fn test_health_check_no_requests_peer_stays_healthy() {
1278 let mut lb = PeerLoadBalancer::default_config();
1279 lb.add_peer(peer_id(1), 10);
1280 lb.run_health_check();
1281 assert!(
1282 lb.peer_state(&peer_id(1))
1283 .expect("test: peer 1 should remain healthy when no requests")
1284 .is_healthy
1285 );
1286 }
1287
1288 #[test]
1289 fn test_health_check_recovers_peer() {
1290 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1291 health_threshold: 0.8,
1292 max_in_flight: 1000,
1293 ..Default::default()
1294 });
1295 lb.add_peer(peer_id(1), 100);
1296 lb.peers
1298 .get_mut(&peer_id(1))
1299 .expect("test: peer 1 should exist for success_count mutation in recovery test")
1300 .success_count = 1;
1301 lb.peers
1302 .get_mut(&peer_id(1))
1303 .expect("test: peer 1 should exist for failure_count mutation in recovery test")
1304 .failure_count = 9;
1305 lb.run_health_check();
1306 assert!(
1307 !lb.peer_state(&peer_id(1))
1308 .expect("test: peer 1 should be unhealthy after bad rate in recovery test")
1309 .is_healthy
1310 );
1311 lb.peers
1313 .get_mut(&peer_id(1))
1314 .expect("test: peer 1 should exist for success_count fix in recovery test")
1315 .success_count = 90;
1316 lb.peers
1317 .get_mut(&peer_id(1))
1318 .expect("test: peer 1 should exist for failure_count fix in recovery test")
1319 .failure_count = 10;
1320 lb.run_health_check();
1321 assert!(
1322 lb.peer_state(&peer_id(1))
1323 .expect("test: peer 1 should be healthy after recovery")
1324 .is_healthy
1325 );
1326 }
1327
1328 #[test]
1331 fn test_cooldown_excludes_peer_from_selection() {
1332 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1333 cooldown_ms: 60_000, ..Default::default()
1335 });
1336 lb.add_peer(peer_id(1), 10);
1337 lb.cooldown_peer(&peer_id(1))
1338 .expect("test: cooldown_peer should succeed");
1339 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
1340 }
1341
1342 #[test]
1343 fn test_cooldown_nonexistent_peer_returns_error() {
1344 let mut lb = PeerLoadBalancer::default_config();
1345 assert_eq!(lb.cooldown_peer(&peer_id(99)), Err(PlbError::PeerNotFound));
1346 }
1347
1348 #[test]
1349 fn test_cooldown_does_not_affect_other_peers() {
1350 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1351 cooldown_ms: 60_000,
1352 strategy: PlbStrategy::RoundRobin,
1353 ..Default::default()
1354 });
1355 lb.add_peer(peer_id(1), 10);
1356 lb.add_peer(peer_id(2), 10);
1357 lb.cooldown_peer(&peer_id(1))
1358 .expect("test: cooldown peer 1 should succeed");
1359 let p = lb
1360 .select_peer(&[])
1361 .expect("test: select_peer should pick non-cooled peer 2");
1362 assert_eq!(p, peer_id(2));
1363 }
1364
1365 #[test]
1366 fn test_cooldown_peer_state_has_future_ts() {
1367 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1368 cooldown_ms: 5000,
1369 ..Default::default()
1370 });
1371 lb.add_peer(peer_id(1), 10);
1372 lb.cooldown_peer(&peer_id(1))
1373 .expect("test: cooldown_peer should set future cooldown timestamp");
1374 let state = lb
1375 .peer_state(&peer_id(1))
1376 .expect("test: peer 1 should exist to check cooldown timestamp");
1377 assert!(state.cooldown_until_ts > 0);
1378 }
1379
1380 #[test]
1383 fn test_stats_zero_on_fresh_balancer() {
1384 let lb = PeerLoadBalancer::default_config();
1385 let s = lb.balancer_stats();
1386 assert_eq!(s.total_requests, 0);
1387 assert_eq!(s.error_rate, 0.0);
1388 assert_eq!(s.avg_latency_ms, 0.0);
1389 assert!(s.peer_stats.is_empty());
1390 }
1391
1392 #[test]
1393 fn test_stats_total_requests_increments() {
1394 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1395 max_in_flight: 1000,
1396 ..Default::default()
1397 });
1398 lb.add_peer(peer_id(1), 100);
1399 for _ in 0..5 {
1400 let p = lb
1401 .select_peer(&[])
1402 .expect("test: select_peer for total_requests stats test");
1403 lb.record_result(&p, 10.0, true)
1404 .expect("test: record_result for total_requests stats test");
1405 }
1406 assert_eq!(lb.balancer_stats().total_requests, 5);
1407 }
1408
1409 #[test]
1410 fn test_stats_error_rate_correct() {
1411 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1412 max_in_flight: 1000,
1413 ..Default::default()
1414 });
1415 lb.add_peer(peer_id(1), 100);
1416 for _ in 0..8 {
1417 let p = lb
1418 .select_peer(&[])
1419 .expect("test: select_peer for error_rate stats test");
1420 lb.record_result(&p, 10.0, true)
1421 .expect("test: record_result success for error_rate stats test");
1422 }
1423 for _ in 0..2 {
1424 let p = lb
1425 .select_peer(&[])
1426 .expect("test: select_peer for error records in error_rate stats test");
1427 lb.record_result(&p, 10.0, false)
1428 .expect("test: record_result failure for error_rate stats test");
1429 }
1430 let s = lb.balancer_stats();
1431 assert!((s.error_rate - 0.2).abs() < 1e-9);
1432 }
1433
1434 #[test]
1435 fn test_stats_avg_latency() {
1436 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1437 max_in_flight: 1000,
1438 ..Default::default()
1439 });
1440 lb.add_peer(peer_id(1), 100);
1441 for _ in 0..4 {
1442 let p = lb
1443 .select_peer(&[])
1444 .expect("test: select_peer for avg_latency stats test");
1445 lb.record_result(&p, 50.0, true)
1446 .expect("test: record_result for avg_latency stats test");
1447 }
1448 let s = lb.balancer_stats();
1449 assert!((s.avg_latency_ms - 50.0).abs() < 1e-9);
1450 }
1451
1452 #[test]
1453 fn test_stats_healthy_peers_count() {
1454 let mut lb = PeerLoadBalancer::default_config();
1455 lb.add_peer(peer_id(1), 10);
1456 lb.add_peer(peer_id(2), 10);
1457 lb.add_peer(peer_id(3), 10);
1458 lb.set_healthy(&peer_id(3), false)
1459 .expect("test: set_healthy false for healthy peers count test");
1460 let s = lb.balancer_stats();
1461 assert_eq!(s.healthy_peers, 2);
1462 }
1463
1464 #[test]
1465 fn test_stats_cooling_peers_count() {
1466 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1467 cooldown_ms: 60_000,
1468 ..Default::default()
1469 });
1470 lb.add_peer(peer_id(1), 10);
1471 lb.add_peer(peer_id(2), 10);
1472 lb.cooldown_peer(&peer_id(1))
1473 .expect("test: cooldown_peer for cooling_peers count test");
1474 let s = lb.balancer_stats();
1475 assert_eq!(s.cooling_peers, 1);
1476 }
1477
1478 #[test]
1479 fn test_stats_peer_stats_populated() {
1480 let mut lb = PeerLoadBalancer::default_config();
1481 lb.add_peer(peer_id(1), 10);
1482 lb.add_peer(peer_id(2), 20);
1483 let s = lb.balancer_stats();
1484 assert_eq!(s.peer_stats.len(), 2);
1485 }
1486
1487 #[test]
1490 fn test_max_in_flight_blocks_selection() {
1491 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1492 max_in_flight: 2,
1493 ..Default::default()
1494 });
1495 lb.add_peer(peer_id(1), 10);
1496 let _p1 = lb
1497 .select_peer(&[])
1498 .expect("test: first select_peer for max_in_flight test");
1499 let _p2 = lb
1500 .select_peer(&[])
1501 .expect("test: second select_peer for max_in_flight test");
1502 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
1504 }
1505
1506 #[test]
1507 fn test_in_flight_released_after_record() {
1508 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1509 max_in_flight: 1,
1510 ..Default::default()
1511 });
1512 lb.add_peer(peer_id(1), 10);
1513 let p = lb
1514 .select_peer(&[])
1515 .expect("test: select_peer for in_flight release test");
1516 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
1517 lb.record_result(&p, 5.0, true)
1518 .expect("test: record_result to release in_flight should succeed");
1519 assert!(lb.select_peer(&[]).is_ok());
1520 }
1521
1522 #[test]
1525 fn test_capacity_blocks_selection_when_full() {
1526 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1527 max_in_flight: 1000,
1528 ..Default::default()
1529 });
1530 lb.add_peer(peer_id(1), 1);
1531 let _p = lb
1532 .select_peer(&[])
1533 .expect("test: first select_peer for capacity enforcement test");
1534 assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
1536 }
1537
1538 #[test]
1541 fn test_error_display_no_healthy_peers() {
1542 let e = PlbError::NoHealthyPeers;
1543 assert!(!e.to_string().is_empty());
1544 }
1545
1546 #[test]
1547 fn test_error_display_peer_not_found() {
1548 let e = PlbError::PeerNotFound;
1549 assert!(!e.to_string().is_empty());
1550 }
1551
1552 #[test]
1555 fn test_strategy_default_is_round_robin() {
1556 assert_eq!(PlbStrategy::default(), PlbStrategy::RoundRobin);
1557 }
1558
1559 #[test]
1562 fn test_fnv1a_64_empty() {
1563 assert_eq!(fnv1a_64(&[]), 14_695_981_039_346_656_037);
1564 }
1565
1566 #[test]
1567 fn test_fnv1a_64_deterministic() {
1568 assert_eq!(fnv1a_64(b"hello"), fnv1a_64(b"hello"));
1569 }
1570
1571 #[test]
1572 fn test_fnv1a_64_different_inputs_differ() {
1573 assert_ne!(fnv1a_64(b"foo"), fnv1a_64(b"bar"));
1574 }
1575
1576 #[test]
1579 fn test_xorshift64_not_zero() {
1580 let mut state = 12345u64;
1581 let v = xorshift64(&mut state);
1582 assert_ne!(v, 0);
1583 }
1584
1585 #[test]
1586 fn test_xorshift64_updates_state() {
1587 let mut state = 12345u64;
1588 xorshift64(&mut state);
1589 assert_ne!(state, 12345);
1590 }
1591
1592 #[test]
1593 fn test_xorshift64_sequence_varies() {
1594 let mut state = 99999u64;
1595 let v1 = xorshift64(&mut state);
1596 let v2 = xorshift64(&mut state);
1597 assert_ne!(v1, v2);
1598 }
1599
1600 #[test]
1603 fn test_peer_id_is_32_bytes() {
1604 let id: PlbPeerId = [0u8; 32];
1605 assert_eq!(id.len(), 32);
1606 }
1607
1608 #[test]
1611 fn test_config_default_health_threshold() {
1612 let cfg = PlbBalancerConfig::default();
1613 assert!((cfg.health_threshold - 0.8).abs() < 1e-9);
1614 }
1615
1616 #[test]
1617 fn test_config_default_max_in_flight() {
1618 let cfg = PlbBalancerConfig::default();
1619 assert_eq!(cfg.max_in_flight, 64);
1620 }
1621
1622 #[test]
1623 fn test_config_default_ewma_alpha() {
1624 let cfg = PlbBalancerConfig::default();
1625 assert!((cfg.ewma_alpha - 0.1).abs() < 1e-9);
1626 }
1627
1628 #[test]
1631 fn test_full_cycle_multiple_peers_and_strategies() {
1632 for strategy in [
1633 PlbStrategy::RoundRobin,
1634 PlbStrategy::WeightedRandom,
1635 PlbStrategy::LeastConnections,
1636 PlbStrategy::LeastLatency,
1637 PlbStrategy::ConsistentHash,
1638 PlbStrategy::PowerOfTwo,
1639 ] {
1640 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1641 strategy,
1642 max_in_flight: 1000,
1643 ..Default::default()
1644 });
1645 lb.add_peer(peer_id(1), 100);
1646 lb.add_peer(peer_id(2), 100);
1647 lb.add_peer(peer_id(3), 100);
1648 for i in 0u8..30 {
1649 let key = [i; 8];
1650 let p = lb
1651 .select_peer(&key)
1652 .expect("test: select_peer in full cycle integration test");
1653 lb.record_result(&p, (i as f64) * 2.0, i % 5 != 0)
1654 .expect("test: record_result in full cycle integration test");
1655 }
1656 let s = lb.balancer_stats();
1657 assert_eq!(s.total_requests, 30);
1658 }
1659 }
1660
1661 #[test]
1662 fn test_remove_peer_during_operation() {
1663 let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1664 max_in_flight: 1000,
1665 ..Default::default()
1666 });
1667 lb.add_peer(peer_id(1), 10);
1668 lb.add_peer(peer_id(2), 10);
1669 let p = lb
1670 .select_peer(&[])
1671 .expect("test: select_peer before remove peer");
1672 lb.record_result(&p, 10.0, true)
1673 .expect("test: record_result before remove peer");
1674 lb.remove_peer(&peer_id(1));
1675 let p2 = lb
1677 .select_peer(&[])
1678 .expect("test: select_peer after remove peer");
1679 lb.record_result(&p2, 5.0, true)
1680 .expect("test: record_result after remove peer");
1681 assert_eq!(lb.peer_count(), 1);
1682 }
1683
1684 #[test]
1685 fn test_type_alias_peer_load_balancer() {
1686 let lb: PlbPeerLoadBalancer = PlbPeerLoadBalancer::default_config();
1687 assert_eq!(lb.peer_count(), 0);
1688 }
1689
1690 #[test]
1691 fn test_peer_state_failure_rate_zero_when_no_history() {
1692 let state = PlbPeerState::new(peer_id(1), 10);
1693 assert_eq!(state.failure_rate(), 0.0);
1694 }
1695
1696 #[test]
1697 fn test_peer_state_failure_rate_calculation() {
1698 let mut state = PlbPeerState::new(peer_id(1), 10);
1699 state.success_count = 3;
1700 state.failure_count = 1;
1701 assert!((state.failure_rate() - 0.25).abs() < 1e-9);
1702 }
1703
1704 #[test]
1705 fn test_peer_state_is_available_respects_cooldown() {
1706 let mut state = PlbPeerState::new(peer_id(1), 10);
1707 state.cooldown_until_ts = u64::MAX;
1708 assert!(!state.is_available(100, 0));
1709 }
1710
1711 #[test]
1712 fn test_peer_state_is_available_when_not_in_cooldown() {
1713 let state = PlbPeerState::new(peer_id(1), 10);
1714 assert!(state.is_available(100, now_ms()));
1715 }
1716}