pub mod messages;
pub mod scaling;
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{sync::Arc, vec::Vec};
#[cfg(feature = "std")]
use std::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
use crate::constants::{SPLITMIX64_GAMMA, SPLITMIX64_MIX_1, SPLITMIX64_MIX_2};
use crate::utils::BasisPoints;
pub use messages::*;
pub use scaling::*;
#[derive(Debug, Clone)]
pub struct InstanceMetrics {
pub servlet_id: Vec<u8>,
pub utilization: BasisPoints,
pub active_requests: u32,
}
pub trait LoadBalancer: Send + Sync + Default + Clone {
fn select(&self, candidates: &[InstanceMetrics]) -> Option<usize>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LeastLoaded;
impl LoadBalancer for LeastLoaded {
fn select(&self, candidates: &[InstanceMetrics]) -> Option<usize> {
if candidates.is_empty() {
return None;
}
candidates
.iter()
.enumerate()
.min_by_key(|(_, m)| m.utilization.get())
.map(|(i, _)| i)
}
}
#[derive(Debug, Clone)]
pub struct PowerOfTwoChoices {
rng: Arc<AtomicU64>,
}
static P2C_SEED_SEQUENCE: AtomicU64 = AtomicU64::new(0);
impl Default for PowerOfTwoChoices {
fn default() -> Self {
let sequence = P2C_SEED_SEQUENCE.fetch_add(SPLITMIX64_GAMMA, Ordering::Relaxed);
Self { rng: Arc::new(AtomicU64::new(sequence ^ current_timestamp_ms())) }
}
}
fn splitmix64_next(state: &AtomicU64) -> u64 {
let mut z = state
.fetch_add(SPLITMIX64_GAMMA, Ordering::Relaxed)
.wrapping_add(SPLITMIX64_GAMMA);
z = (z ^ (z >> 30)).wrapping_mul(SPLITMIX64_MIX_1);
z = (z ^ (z >> 27)).wrapping_mul(SPLITMIX64_MIX_2);
z ^ (z >> 31)
}
impl LoadBalancer for PowerOfTwoChoices {
fn select(&self, candidates: &[InstanceMetrics]) -> Option<usize> {
match candidates.len() {
0 => None,
1 => Some(0),
2 => {
if candidates[0].utilization <= candidates[1].utilization {
Some(0)
} else {
Some(1)
}
}
n => {
let draw = splitmix64_next(&self.rng);
let first = ((draw >> 32) as usize) % n;
let offset = ((draw & u64::from(u32::MAX)) as usize) % (n - 1);
let second = offset + usize::from(offset >= first);
if candidates[first].utilization <= candidates[second].utilization {
Some(first)
} else {
Some(second)
}
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RoundRobin {
counter: Arc<AtomicU64>,
}
impl LoadBalancer for RoundRobin {
fn select(&self, candidates: &[InstanceMetrics]) -> Option<usize> {
if candidates.is_empty() {
return None;
}
let count = self.counter.fetch_add(1, Ordering::Relaxed);
Some((count as usize) % candidates.len())
}
}
pub trait ScoringPolicy: Send + Sync {
fn score(&self, pheromone: u64, utilization: BasisPoints) -> BasisPoints;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PheromoneScoring;
impl ScoringPolicy for PheromoneScoring {
fn score(&self, pheromone: u64, _utilization: BasisPoints) -> BasisPoints {
let inverted = 10000u64.saturating_sub(pheromone);
BasisPoints::new_saturating(inverted as u16)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UtilizationScoring;
impl ScoringPolicy for UtilizationScoring {
fn score(&self, _pheromone: u64, utilization: BasisPoints) -> BasisPoints {
utilization
}
}
#[derive(Debug, Clone, Copy)]
pub struct CombinedScoring {
pub pheromone_weight: u16,
}
impl Default for CombinedScoring {
fn default() -> Self {
Self { pheromone_weight: 5000 } }
}
impl ScoringPolicy for CombinedScoring {
fn score(&self, pheromone: u64, utilization: BasisPoints) -> BasisPoints {
let pheromone_score = 10000u64.saturating_sub(pheromone);
let util_score = utilization.get() as u64;
let pw = self.pheromone_weight as u64;
let uw = 10000u64.saturating_sub(pw);
let combined = (pheromone_score * pw + util_score * uw) / 10000;
BasisPoints::new_saturating(combined as u16)
}
}
pub type MessageValidator = fn(&[u8]) -> bool;
pub trait MessageRouter: Send + Sync + Default + Clone {
fn route<'a>(&self, message: &[u8], registered_types: &'a [(&'static [u8], MessageValidator)]) -> Option<&'a [u8]>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TypeBasedRouter;
impl MessageRouter for TypeBasedRouter {
fn route<'a>(&self, message: &[u8], registered_types: &'a [(&'static [u8], MessageValidator)]) -> Option<&'a [u8]> {
registered_types
.iter()
.find(|(_, validator)| validator(message))
.map(|(name, _)| *name)
}
}
pub fn current_timestamp_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn aggregate_utilization(total_utilization: u64, instance_count: usize) -> BasisPoints {
match instance_count {
0 => BasisPoints::MAX,
n => BasisPoints::new_saturating((total_utilization / n as u64) as u16),
}
}
pub fn reply_frame<M: crate::Message>(
id: impl AsRef<[u8]>,
message: M,
) -> Result<Option<crate::Frame>, crate::TightBeamError> {
use crate::builder::TypeBuilder;
let frame = crate::utils::compose(crate::Version::V0)
.with_id(id)
.with_order(0)
.with_message(message)
.build()?;
Ok(Some(frame))
}
pub fn reply_frame_with_priority<M: crate::Message>(
id: impl AsRef<[u8]>,
priority: crate::MessagePriority,
message: M,
) -> Result<Option<crate::Frame>, crate::TightBeamError> {
use crate::builder::TypeBuilder;
let frame = crate::utils::compose(crate::Version::V2)
.with_id(id)
.with_order(0)
.with_priority(priority)
.with_message(message)
.build()?;
Ok(Some(frame))
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::{InstanceMetrics, LoadBalancer, PowerOfTwoChoices};
use crate::utils::BasisPoints;
fn candidates(count: usize) -> Vec<InstanceMetrics> {
(0..count)
.map(|index| InstanceMetrics {
servlet_id: vec![index as u8],
utilization: BasisPoints::new(5000),
active_requests: 0,
})
.collect()
}
#[test]
fn p2c_returns_none_for_empty_pool() {
let balancer = PowerOfTwoChoices::default();
assert_eq!(balancer.select(&[]), None);
}
#[test]
fn p2c_returns_sole_candidate() {
let balancer = PowerOfTwoChoices::default();
assert_eq!(balancer.select(&candidates(1)), Some(0));
}
#[test]
fn p2c_picks_least_loaded_of_two() {
let balancer = PowerOfTwoChoices::default();
let mut pool = candidates(2);
pool[1].utilization = BasisPoints::new(100);
assert_eq!(balancer.select(&pool), Some(1));
}
#[test]
fn p2c_covers_all_indices_under_uniform_load() {
let balancer = PowerOfTwoChoices::default();
let pool = candidates(8);
let seen: HashSet<usize> = (0..4096).filter_map(|_| balancer.select(&pool)).collect();
assert_eq!(seen.len(), pool.len());
}
#[test]
fn p2c_escapes_loaded_leading_pair() {
let balancer = PowerOfTwoChoices::default();
let mut pool = candidates(4);
pool[0].utilization = BasisPoints::MAX;
pool[1].utilization = BasisPoints::MAX;
let seen: HashSet<usize> = (0..4096).filter_map(|_| balancer.select(&pool)).collect();
assert!(seen.contains(&2));
assert!(seen.contains(&3));
}
#[test]
fn p2c_balancers_draw_distinct_streams() {
let first = PowerOfTwoChoices::default();
let second = PowerOfTwoChoices::default();
let pool = candidates(64);
let first_picks: Vec<Option<usize>> = (0..16).map(|_| first.select(&pool)).collect();
let second_picks: Vec<Option<usize>> = (0..16).map(|_| second.select(&pool)).collect();
assert_ne!(first_picks, second_picks);
}
const AGGREGATE_CASES: &[(u64, usize, u16)] = &[
(0, 0, 10000), (0, 4, 0), (20000, 4, 5000), (10000, 2, 5000), (40000, 4, 10000), ];
#[test]
fn aggregate_utilization_means_across_all_instances() {
for &(total, count, expected) in AGGREGATE_CASES {
assert_eq!(super::aggregate_utilization(total, count).get(), expected);
}
}
}