use parking_lot::RwLock;
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct MirrorManager {
urls: Vec<String>,
current_index: AtomicUsize,
failed: RwLock<Vec<usize>>,
failure_counts: RwLock<Vec<u32>>,
max_failures: u32,
}
impl MirrorManager {
pub fn new(primary_url: String, mirrors: Vec<String>) -> Self {
let mut urls = vec![primary_url];
urls.extend(mirrors);
let len = urls.len();
Self {
urls,
current_index: AtomicUsize::new(0),
failed: RwLock::new(Vec::new()),
failure_counts: RwLock::new(vec![0; len]),
max_failures: 3,
}
}
pub fn single(url: String) -> Self {
Self::new(url, Vec::new())
}
pub fn current_url(&self) -> &str {
let idx = self.current_index.load(Ordering::Relaxed);
&self.urls[idx]
}
pub fn url_for_segment(&self, segment_idx: usize) -> &str {
let available = self.available_urls();
if available.is_empty() {
return self.current_url(); }
let idx = segment_idx % available.len();
available[idx]
}
pub fn available_urls(&self) -> Vec<&str> {
let failed = self.failed.read();
self.urls
.iter()
.enumerate()
.filter(|(i, _)| !failed.contains(i))
.map(|(_, url)| url.as_str())
.collect()
}
pub fn has_available(&self) -> bool {
self.failed.read().len() < self.urls.len()
}
pub fn available_count(&self) -> usize {
self.urls.len() - self.failed.read().len()
}
pub fn report_failure(&self) -> Option<&str> {
let current = self.current_index.load(Ordering::Relaxed);
{
let mut counts = self.failure_counts.write();
counts[current] += 1;
if counts[current] >= self.max_failures {
let mut failed = self.failed.write();
if !failed.contains(¤t) {
failed.push(current);
tracing::warn!(
"Mirror {} marked as failed after {} failures",
&self.urls[current],
self.max_failures
);
}
}
}
self.switch_to_next()
}
pub fn report_url_failure(&self, url: &str) -> Option<&str> {
if let Some(idx) = self.urls.iter().position(|u| u == url) {
let mut counts = self.failure_counts.write();
counts[idx] += 1;
if counts[idx] >= self.max_failures {
let mut failed = self.failed.write();
if !failed.contains(&idx) {
failed.push(idx);
tracing::warn!(
"Mirror {} marked as failed after {} failures",
url,
self.max_failures
);
}
}
if self.current_index.load(Ordering::Relaxed) == idx {
return self.switch_to_next();
}
}
Some(self.current_url())
}
pub fn failover_from(&self, url: &str) -> Option<&str> {
let idx = self.urls.iter().position(|candidate| candidate == url)?;
{
let mut failed = self.failed.write();
if !failed.contains(&idx) {
failed.push(idx);
}
}
if self.current_index.load(Ordering::Relaxed) == idx {
self.switch_to_next()
} else {
Some(self.current_url())
}
}
fn switch_to_next(&self) -> Option<&str> {
let failed = self.failed.read();
let current = self.current_index.load(Ordering::Relaxed);
for offset in 1..self.urls.len() {
let idx = (current + offset) % self.urls.len();
if !failed.contains(&idx) {
self.current_index.store(idx, Ordering::Relaxed);
tracing::info!("Switched to mirror: {}", &self.urls[idx]);
return Some(&self.urls[idx]);
}
}
None }
pub fn report_success(&self) {
let current = self.current_index.load(Ordering::Relaxed);
self.failure_counts.write()[current] = 0;
}
pub fn reset(&self) {
*self.failed.write() = Vec::new();
*self.failure_counts.write() = vec![0; self.urls.len()];
self.current_index.store(0, Ordering::Relaxed);
}
pub fn all_urls(&self) -> &[String] {
&self.urls
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_url() {
let mgr = MirrorManager::single("http://example.com/file.zip".to_string());
assert_eq!(mgr.current_url(), "http://example.com/file.zip");
assert_eq!(mgr.available_count(), 1);
}
#[test]
fn test_multiple_mirrors() {
let mgr = MirrorManager::new(
"http://primary.com/file.zip".to_string(),
vec![
"http://mirror1.com/file.zip".to_string(),
"http://mirror2.com/file.zip".to_string(),
],
);
assert_eq!(mgr.current_url(), "http://primary.com/file.zip");
assert_eq!(mgr.available_count(), 3);
}
#[test]
fn test_failover() {
let mgr = MirrorManager::new(
"http://primary.com/file.zip".to_string(),
vec!["http://mirror.com/file.zip".to_string()],
);
for _ in 0..3 {
mgr.report_url_failure("http://primary.com/file.zip");
}
assert_eq!(mgr.current_url(), "http://mirror.com/file.zip");
assert_eq!(mgr.available_count(), 1);
}
#[test]
fn test_round_robin_segments() {
let mgr = MirrorManager::new(
"http://primary.com/file.zip".to_string(),
vec![
"http://mirror1.com/file.zip".to_string(),
"http://mirror2.com/file.zip".to_string(),
],
);
let url0 = mgr.url_for_segment(0);
let url1 = mgr.url_for_segment(1);
let url2 = mgr.url_for_segment(2);
let url3 = mgr.url_for_segment(3);
assert_eq!(url0, "http://primary.com/file.zip");
assert_eq!(url1, "http://mirror1.com/file.zip");
assert_eq!(url2, "http://mirror2.com/file.zip");
assert_eq!(url3, "http://primary.com/file.zip"); }
#[test]
fn test_all_failed() {
let mgr = MirrorManager::single("http://example.com/file.zip".to_string());
for _ in 0..3 {
mgr.report_failure();
}
assert!(!mgr.has_available());
assert_eq!(mgr.available_count(), 0);
}
#[test]
fn test_reset() {
let mgr = MirrorManager::new(
"http://primary.com/file.zip".to_string(),
vec!["http://mirror.com/file.zip".to_string()],
);
for _ in 0..6 {
mgr.report_failure();
}
assert!(!mgr.has_available());
mgr.reset();
assert!(mgr.has_available());
assert_eq!(mgr.available_count(), 2);
assert_eq!(mgr.current_url(), "http://primary.com/file.zip");
}
#[test]
fn test_immediate_failover_switches_sources() {
let mgr = MirrorManager::new(
"http://primary.com/file.zip".to_string(),
vec!["http://mirror.com/file.zip".to_string()],
);
let next = mgr.failover_from("http://primary.com/file.zip");
assert_eq!(next, Some("http://mirror.com/file.zip"));
assert_eq!(mgr.current_url(), "http://mirror.com/file.zip");
}
}