Skip to main content

ipfrs_network/
peer_load_balancer.rs

1//! Peer-aware load balancer for IPFRS network layer.
2//!
3//! Distributes requests across peers based on capacity, health, and observed latency.
4//! Supports six selection strategies including consistent hashing and power-of-two choices.
5
6use std::collections::{HashMap, VecDeque};
7
8// ── Type aliases ─────────────────────────────────────────────────────────────
9
10/// 32-byte peer identifier used by the load balancer.
11pub type PlbPeerId = [u8; 32];
12
13// ── Error type ────────────────────────────────────────────────────────────────
14
15/// Errors produced by the load balancer.
16#[derive(Debug, Clone, PartialEq, thiserror::Error)]
17pub enum PlbError {
18    /// No healthy peers are available for selection.
19    #[error("no healthy peers available")]
20    NoHealthyPeers,
21    /// The requested peer was not found in the balancer.
22    #[error("peer not found")]
23    PeerNotFound,
24    /// An arithmetic overflow occurred during weight computation.
25    #[error("weight computation overflow")]
26    WeightOverflow,
27}
28
29// ── Strategy enum ─────────────────────────────────────────────────────────────
30
31/// Load-balancing selection strategy.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum PlbStrategy {
34    /// Simple round-robin over healthy peers.
35    #[default]
36    RoundRobin,
37    /// Weighted random selection proportional to peer weight.
38    WeightedRandom,
39    /// Always select the peer with fewest in-flight requests.
40    LeastConnections,
41    /// Always select the peer with lowest average latency.
42    LeastLatency,
43    /// FNV-1a consistent hash on the request key with virtual nodes.
44    ConsistentHash,
45    /// Sample two random healthy peers and pick the better one.
46    PowerOfTwo,
47}
48
49// ── Configuration ─────────────────────────────────────────────────────────────
50
51/// Configuration for [`PeerLoadBalancer`].
52#[derive(Debug, Clone)]
53pub struct PlbBalancerConfig {
54    /// Strategy used for peer selection.
55    pub strategy: PlbStrategy,
56    /// Fraction of successes required for a peer to remain healthy (0.0–1.0).
57    /// e.g. 0.8 means a peer must have ≥ 80 % success rate.
58    pub health_threshold: f64,
59    /// Maximum in-flight requests allowed per peer.
60    pub max_in_flight: u32,
61    /// Duration in milliseconds a peer stays in cooldown after being penalised.
62    pub cooldown_ms: u64,
63    /// Number of virtual nodes per peer for consistent-hash ring.
64    pub virtual_nodes: u32,
65    /// EWMA smoothing factor α for latency updates (0 < α ≤ 1).
66    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// ── Per-peer state ────────────────────────────────────────────────────────────
83
84/// Runtime state for a single peer tracked by the balancer.
85#[derive(Debug, Clone)]
86pub struct PlbPeerState {
87    /// Peer identifier.
88    pub id: PlbPeerId,
89    /// Maximum concurrent requests this peer can handle.
90    pub capacity: u32,
91    /// Currently in-flight requests directed at this peer.
92    pub in_flight: u32,
93    /// Cumulative successful responses.
94    pub success_count: u64,
95    /// Cumulative failed responses.
96    pub failure_count: u64,
97    /// Exponentially weighted moving average of response latency (ms).
98    pub avg_latency_ms: f64,
99    /// Unix-ms timestamp of the last request sent to this peer.
100    pub last_used_ts: u64,
101    /// Whether this peer is currently considered healthy.
102    pub is_healthy: bool,
103    /// Normalised selection weight in [0, 1].
104    pub weight: f64,
105    /// Unix-ms timestamp at which the cooldown expires (0 = not in cooldown).
106    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    /// True when the peer is available for selection.
126    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    /// Failure rate in [0, 1].  Returns 0 when no requests have been observed.
134    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// ── Request record ─────────────────────────────────────────────────────────────
145
146/// A single completed request record stored in the rolling log.
147#[derive(Debug, Clone)]
148pub struct PlbRequestRecord {
149    /// Unix-ms timestamp when the request completed.
150    pub ts: u64,
151    /// Peer that handled the request.
152    pub peer_id: PlbPeerId,
153    /// Observed round-trip latency (ms).
154    pub latency_ms: f64,
155    /// Whether the request succeeded.
156    pub success: bool,
157}
158
159// ── Aggregate statistics ──────────────────────────────────────────────────────
160
161/// Per-peer statistics snapshot.
162#[derive(Debug, Clone)]
163pub struct PlbPeerStats {
164    /// Peer identifier.
165    pub id: PlbPeerId,
166    /// Current in-flight requests.
167    pub in_flight: u32,
168    /// Cumulative successes.
169    pub success_count: u64,
170    /// Cumulative failures.
171    pub failure_count: u64,
172    /// Current average latency (ms).
173    pub avg_latency_ms: f64,
174    /// Current normalised weight.
175    pub weight: f64,
176    /// Whether healthy.
177    pub is_healthy: bool,
178}
179
180/// Aggregate statistics for the whole balancer.
181#[derive(Debug, Clone)]
182pub struct PlbBalancerStats {
183    /// Total requests recorded since creation.
184    pub total_requests: u64,
185    /// Fraction of requests that failed (0.0–1.0).
186    pub error_rate: f64,
187    /// Average latency across all recorded requests (ms).
188    pub avg_latency_ms: f64,
189    /// Per-peer statistics.
190    pub peer_stats: Vec<PlbPeerStats>,
191    /// Number of healthy peers.
192    pub healthy_peers: usize,
193    /// Number of peers in cooldown.
194    pub cooling_peers: usize,
195}
196
197// ── PRNG helpers ──────────────────────────────────────────────────────────────
198
199/// XorShift64 pseudo-random number generator.
200#[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/// FNV-1a 64-bit hash.
211#[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// ── Virtual-node ring for consistent hashing ─────────────────────────────────
222
223/// A single entry in the consistent-hash ring.
224#[derive(Debug, Clone)]
225struct RingEntry {
226    hash: u64,
227    peer_id: PlbPeerId,
228}
229
230// ── Main balancer struct ──────────────────────────────────────────────────────
231
232/// Peer-aware load balancer.
233///
234/// Distributes requests across known peers using a configurable [`PlbStrategy`].
235/// Tracks health, latency, and in-flight requests per peer and recomputes
236/// normalised weights after every completed request.
237pub struct PeerLoadBalancer {
238    peers: HashMap<PlbPeerId, PlbPeerState>,
239    request_log: VecDeque<PlbRequestRecord>,
240    config: PlbBalancerConfig,
241    /// Round-robin cursor (index into sorted healthy peer list).
242    rr_cursor: usize,
243    /// PRNG state for weighted-random and power-of-two strategies.
244    prng_state: u64,
245    /// Total requests dispatched.
246    total_requests: u64,
247    /// Consistent-hash ring (sorted by hash).
248    ring: Vec<RingEntry>,
249    /// True when the ring needs rebuilding.
250    ring_dirty: bool,
251}
252
253impl PeerLoadBalancer {
254    /// Maximum entries kept in the request log.
255    const LOG_CAPACITY: usize = 2_000;
256
257    /// Create a new balancer with the given configuration.
258    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    /// Create a balancer with default configuration.
272    pub fn default_config() -> Self {
273        Self::new(PlbBalancerConfig::default())
274    }
275
276    // ── Peer management ───────────────────────────────────────────────────────
277
278    /// Register a new peer with the given capacity.
279    /// If the peer already exists its capacity is updated.
280    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    /// Remove a peer from the balancer.
290    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    /// Manually override the health state of a peer.
300    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    /// Put a peer in cooldown for `config.cooldown_ms` milliseconds.
308    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    // ── Peer selection ────────────────────────────────────────────────────────
317
318    /// Select a peer for the next request.
319    ///
320    /// `key` is used only by [`PlbStrategy::ConsistentHash`]; other strategies ignore it.
321    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    // ── Result recording ──────────────────────────────────────────────────────
341
342    /// Record the outcome of a completed request.
343    ///
344    /// Updates EWMA latency, success/failure counters and recomputes weights.
345    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            // Decrement in-flight
355            peer.in_flight = peer.in_flight.saturating_sub(1);
356            // EWMA latency update
357            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        // Append to log
369        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    // ── Weight computation ────────────────────────────────────────────────────
384
385    /// Recompute normalised weights for all peers.
386    ///
387    /// Weight formula per peer:
388    /// `w = capacity_score × latency_score × health_score`
389    ///
390    /// where:
391    /// - `capacity_score = (capacity - in_flight) / capacity`  (0 if zero capacity)
392    /// - `latency_score = 1 / (1 + avg_latency_ms / 1000)`
393    /// - `health_score = success_rate` (1.0 for new peers with no history)
394    pub fn recompute_weights(&mut self) {
395        // Collect raw scores
396        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        // Normalise
420        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    // ── Health check ──────────────────────────────────────────────────────────
430
431    /// Evaluate all peers and mark unhealthy those whose failure rate exceeds
432    /// `1 - health_threshold`.
433    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                    // Mark unhealthy if failure rate exceeds 1-threshold
442                    peer.is_healthy = failure_rate <= 1.0 - threshold;
443                }
444            }
445        }
446        self.recompute_weights();
447    }
448
449    // ── Statistics ────────────────────────────────────────────────────────────
450
451    /// Return an aggregate snapshot of balancer statistics.
452    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    /// Return the current configuration.
500    pub fn config(&self) -> &PlbBalancerConfig {
501        &self.config
502    }
503
504    /// Return the number of registered peers.
505    pub fn peer_count(&self) -> usize {
506        self.peers.len()
507    }
508
509    /// Retrieve state for a specific peer.
510    pub fn peer_state(&self, id: &PlbPeerId) -> Option<&PlbPeerState> {
511        self.peers.get(id)
512    }
513
514    // ── Private selection helpers ─────────────────────────────────────────────
515
516    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        // Sort for determinism
530        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            // Fall back to uniform
548            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        // Fallback: last element
560        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        // Rebuild ring if dirty
587        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        // Binary search for first entry >= h
595        let pos = self.ring.partition_point(|entry| entry.hash < h);
596        // Walk the ring starting at pos, skip unavailable peers
597        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        // Pick two distinct random indices
619        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        // Choose the one with lower in-flight count; break ties by latency
627        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
670// ── Timestamp helper ──────────────────────────────────────────────────────────
671
672fn 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
680// ── Type aliases (public re-exports required by the task) ─────────────────────
681
682/// Alias: the main load balancer type.
683pub type PlbPeerLoadBalancer = PeerLoadBalancer;
684
685// ── Tests ─────────────────────────────────────────────────────────────────────
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    // Helper: build a 32-byte id from a u8
692    fn peer_id(b: u8) -> PlbPeerId {
693        let mut id = [0u8; 32];
694        id[0] = b;
695        id
696    }
697
698    // Helper: balancer with a single strategy
699    fn balancer(strategy: PlbStrategy) -> PeerLoadBalancer {
700        PeerLoadBalancer::new(PlbBalancerConfig {
701            strategy,
702            ..Default::default()
703        })
704    }
705
706    // ── Construction & config ─────────────────────────────────────────────────
707
708    #[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    // ── add_peer / remove_peer ────────────────────────────────────────────────
736
737    #[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    // ── set_healthy ───────────────────────────────────────────────────────────
793
794    #[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    // ── select_peer: no peers ─────────────────────────────────────────────────
841
842    #[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    // ── Round-Robin ───────────────────────────────────────────────────────────
849
850    #[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            // decrement in_flight so max_in_flight is not hit
863            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    // ── LeastConnections ─────────────────────────────────────────────────────
884
885    #[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        // Manually inflate in_flight of peer 1
891        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        // Both have 0 in_flight; any result is acceptable
907        assert!(lb.select_peer(&[]).is_ok());
908    }
909
910    // ── LeastLatency ─────────────────────────────────────────────────────────
911
912    #[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        // Both have 0 ms latency; either is fine
937        assert!(lb.select_peer(&[]).is_ok());
938    }
939
940    // ── WeightedRandom ────────────────────────────────────────────────────────
941
942    #[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    // ── ConsistentHash ────────────────────────────────────────────────────────
971
972    #[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        // Just verify no error
999        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"); // triggers ring build
1015        lb.add_peer(peer_id(2), 10); // marks ring dirty
1016        assert!(lb.ring_dirty);
1017        let _ = lb.select_peer(b"x"); // rebuilds
1018        assert!(!lb.ring_dirty);
1019    }
1020
1021    // ── PowerOfTwo ────────────────────────────────────────────────────────────
1022
1023    #[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        // Force PRNG to always pick indices 0 and 1 by seeding predictably
1047        // Just verify it runs without errors many times
1048        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    // ── record_result ─────────────────────────────────────────────────────────
1058
1059    #[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    // ── recompute_weights ─────────────────────────────────────────────────────
1181
1182    #[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        // Weight is based on stats, health doesn't zero it in recompute — it's
1210        // health_score that does that.  An unhealthy peer with 100% successes
1211        // still gets a weight (but is excluded from selection).
1212        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        // Weight can be non-zero even when unhealthy (selection is blocked separately)
1218        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    // ── run_health_check ──────────────────────────────────────────────────────
1227
1228    #[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        // Make it fail first
1297        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        // Fix success rate
1312        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    // ── cooldown_peer ─────────────────────────────────────────────────────────
1329
1330    #[test]
1331    fn test_cooldown_excludes_peer_from_selection() {
1332        let mut lb = PeerLoadBalancer::new(PlbBalancerConfig {
1333            cooldown_ms: 60_000, // 1 minute
1334            ..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    // ── balancer_stats ────────────────────────────────────────────────────────
1381
1382    #[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    // ── max_in_flight enforcement ─────────────────────────────────────────────
1488
1489    #[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        // Now in_flight == 2 == max_in_flight
1503        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    // ── capacity enforcement ──────────────────────────────────────────────────
1523
1524    #[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        // in_flight == capacity == 1
1535        assert_eq!(lb.select_peer(&[]), Err(PlbError::NoHealthyPeers));
1536    }
1537
1538    // ── PlbError display ──────────────────────────────────────────────────────
1539
1540    #[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    // ── PlbStrategy default ───────────────────────────────────────────────────
1553
1554    #[test]
1555    fn test_strategy_default_is_round_robin() {
1556        assert_eq!(PlbStrategy::default(), PlbStrategy::RoundRobin);
1557    }
1558
1559    // ── fnv1a_64 ──────────────────────────────────────────────────────────────
1560
1561    #[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    // ── xorshift64 ────────────────────────────────────────────────────────────
1577
1578    #[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    // ── PlbPeerId type ────────────────────────────────────────────────────────
1601
1602    #[test]
1603    fn test_peer_id_is_32_bytes() {
1604        let id: PlbPeerId = [0u8; 32];
1605        assert_eq!(id.len(), 32);
1606    }
1607
1608    // ── PlbBalancerConfig defaults ────────────────────────────────────────────
1609
1610    #[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    // ── Integration: full request cycle ──────────────────────────────────────
1629
1630    #[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        // Should still work with remaining peer
1676        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}