use super::server::CdnServer;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
struct ServerState {
server: CdnServer,
failures: AtomicU32,
available_after: AtomicU64,
}
pub struct CdnServerPool {
servers: Vec<ServerState>,
epoch: Instant,
next: AtomicU32,
}
impl CdnServerPool {
pub fn new(servers: Vec<CdnServer>) -> Self {
assert!(
!servers.is_empty(),
"CdnServerPool requires at least one server"
);
let epoch = Instant::now();
Self {
servers: servers
.into_iter()
.map(|s| ServerState {
server: s,
failures: AtomicU32::new(0),
available_after: AtomicU64::new(0),
})
.collect(),
epoch,
next: AtomicU32::new(0),
}
}
fn now_nanos(&self) -> u64 {
self.epoch.elapsed().as_nanos() as u64
}
pub fn pick(&self) -> (&CdnServer, Duration) {
let n = self.servers.len();
let now = self.now_nanos();
let start = self.next.fetch_add(1, Ordering::Relaxed) as usize;
for i in 0..n {
let idx = (start + i) % n;
let state = &self.servers[idx];
if state.available_after.load(Ordering::Relaxed) <= now {
return (&state.server, Duration::ZERO);
}
}
let mut best = 0;
let mut best_time = u64::MAX;
for (i, state) in self.servers.iter().enumerate() {
let t = state.available_after.load(Ordering::Relaxed);
if t < best_time {
best_time = t;
best = i;
}
}
let wait = Duration::from_nanos(best_time.saturating_sub(now));
(&self.servers[best].server, wait)
}
pub fn report_success(&self, server: &CdnServer) {
if let Some(state) = self.find(server) {
state.failures.store(0, Ordering::Relaxed);
}
}
pub fn report_failure(&self, server: &CdnServer, retry_after: Option<Duration>) {
if let Some(state) = self.find(server) {
let count = state.failures.fetch_add(1, Ordering::Relaxed) + 1;
let cooldown = match retry_after {
Some(d) => d,
None => {
let secs = (2u64 << count.min(4)).min(30);
Duration::from_secs(secs)
}
};
let deadline = self.now_nanos() + cooldown.as_nanos() as u64;
state.available_after.store(deadline, Ordering::Relaxed);
}
}
pub fn len(&self) -> usize {
self.servers.len()
}
pub fn is_empty(&self) -> bool {
self.servers.is_empty()
}
fn find(&self, server: &CdnServer) -> Option<&ServerState> {
self.servers
.iter()
.find(|s| s.server.host == server.host && s.server.port == server.port)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn server(name: &str) -> CdnServer {
CdnServer {
host: name.into(),
port: 443,
https: true,
vhost: name.into(),
}
}
#[test]
fn pick_round_robins_across_servers() {
let pool = CdnServerPool::new(vec![server("a"), server("b"), server("c")]);
let (s1, w1) = pool.pick();
let (s2, w2) = pool.pick();
let (s3, w3) = pool.pick();
assert_eq!(w1, Duration::ZERO);
assert_eq!(w2, Duration::ZERO);
assert_eq!(w3, Duration::ZERO);
let hosts: Vec<&str> = vec![&s1.host, &s2.host, &s3.host]
.into_iter()
.map(|s| s.as_str())
.collect();
assert!(hosts.contains(&"a"));
assert!(hosts.contains(&"b"));
assert!(hosts.contains(&"c"));
}
#[test]
fn pick_skips_server_in_cooldown() {
let pool = CdnServerPool::new(vec![server("a"), server("b")]);
pool.report_failure(&server("a"), Some(Duration::from_secs(60)));
for _ in 0..5 {
let (s, wait) = pool.pick();
assert_eq!(s.host, "b");
assert_eq!(wait, Duration::ZERO);
}
}
#[test]
fn pick_returns_wait_when_all_in_cooldown() {
let pool = CdnServerPool::new(vec![server("a"), server("b")]);
pool.report_failure(&server("a"), Some(Duration::from_secs(60)));
pool.report_failure(&server("b"), Some(Duration::from_secs(30)));
let (_s, wait) = pool.pick();
assert!(wait > Duration::ZERO);
assert!(wait <= Duration::from_secs(30));
}
#[test]
fn report_success_resets_failure_count() {
let pool = CdnServerPool::new(vec![server("a"), server("b")]);
pool.report_failure(&server("a"), None);
pool.report_failure(&server("a"), None);
pool.report_success(&server("a"));
let (s, wait) = pool.pick();
assert!(s.host == "a" || s.host == "b");
let _ = wait;
}
#[test]
fn report_failure_escalates_cooldown() {
let pool = CdnServerPool::new(vec![server("a"), server("b")]);
pool.report_failure(&server("a"), None); pool.report_failure(&server("a"), None); pool.report_failure(&server("a"), None); pool.report_failure(&server("a"), None); pool.report_failure(&server("a"), None);
let (s, wait) = pool.pick();
assert_eq!(s.host, "b");
assert_eq!(wait, Duration::ZERO);
}
#[test]
fn report_failure_with_retry_after_uses_exact_duration() {
let pool = CdnServerPool::new(vec![server("a"), server("b")]);
pool.report_failure(&server("a"), Some(Duration::from_secs(10)));
let (s, _) = pool.pick();
assert_eq!(s.host, "b");
}
#[test]
fn single_server_pool_returns_wait_on_cooldown() {
let pool = CdnServerPool::new(vec![server("only")]);
pool.report_failure(&server("only"), Some(Duration::from_secs(5)));
let (s, wait) = pool.pick();
assert_eq!(s.host, "only");
assert!(wait > Duration::ZERO);
}
}