use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RotationPolicy {
Plain,
Lap,
}
#[derive(Debug, Default)]
struct HostRotation {
slot: AtomicUsize,
}
impl HostRotation {
fn active_front_index(&self, policy: RotationPolicy) -> Option<usize> {
match self.slot.load(Ordering::Relaxed) {
0 if policy == RotationPolicy::Lap => None,
0 => Some(0),
slot => Some(slot - 1),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct RotationManager {
hosts: Arc<[HostRotation]>,
}
impl RotationManager {
pub(crate) fn new(num_hosts: usize) -> Self {
Self {
hosts: (0..num_hosts).map(|_| HostRotation::default()).collect(),
}
}
pub(crate) fn as_str<'a>(&self, idx: usize, url: &'a Url, lap: bool) -> &'a str {
let policy = if lap {
RotationPolicy::Lap
} else {
RotationPolicy::Plain
};
self.hosts[idx]
.active_front_index(policy)
.and_then(|index| url.fronts().and_then(|fronts| fronts.get(index)))
.map(|front| front.as_str())
.unwrap_or_else(|| url.inner_url().as_str())
}
pub(crate) fn front_str<'a>(&self, idx: usize, url: &'a Url) -> Option<&'a str> {
let index = self.hosts[idx].active_front_index(RotationPolicy::Plain)?;
url.fronts()?.get(index)?.host_str()
}
pub(crate) fn update(&self, idx: usize, url: &Url) -> bool {
if let Some(fronts) = url.fronts()
&& fronts.len() > 1
{
let state = &self.hosts[idx];
let current = state
.active_front_index(RotationPolicy::Plain)
.expect("Plain policy always yields a front index");
let next = (current + 1) % fronts.len();
state.slot.store(next + 1, Ordering::Relaxed);
return next == 0;
}
true
}
pub(crate) fn take_rotation_turn(&self, idx: usize, url: &Url) -> bool {
let Some(fronts) = url.fronts() else {
return false;
};
if fronts.is_empty() {
return false;
}
let state = &self.hosts[idx];
let total_turns = fronts.len() + 1;
let current = state.slot.load(Ordering::Relaxed);
let next = (current + 1) % total_turns;
state.slot.store(next, Ordering::Relaxed);
next != 0
}
pub(crate) fn active_rotation_front_str<'a>(
&self,
idx: usize,
url: &'a Url,
) -> Option<&'a str> {
let index = self.hosts[idx].active_front_index(RotationPolicy::Lap)?;
url.fronts()?.get(index)?.host_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn url(fronts: Option<Vec<&str>>) -> Url {
Url::new("https://a.test", fronts).unwrap()
}
#[test]
fn front_str_starts_at_the_first_configured_front() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(1);
assert_eq!(mgr.front_str(0, &url), Some("f0.test"));
}
#[test]
fn front_str_tracks_take_rotation_turn_once_off_the_direct_slot() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(1);
assert!(mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), Some("f0.test"));
assert_eq!(mgr.front_str(0, &url), Some("f0.test"));
assert!(mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), Some("f1.test"));
assert_eq!(mgr.front_str(0, &url), Some("f1.test"));
}
#[test]
fn front_str_and_active_rotation_front_str_disagree_on_the_untouched_slot() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(1);
assert_eq!(mgr.front_str(0, &url), Some("f0.test"));
assert_eq!(mgr.active_rotation_front_str(0, &url), None);
}
#[test]
fn front_str_is_none_without_fronts() {
let url = url(None);
let mgr = RotationManager::new(1);
assert_eq!(mgr.front_str(0, &url), None);
}
#[test]
fn update_without_multiple_fronts_always_reports_wrapped() {
let mgr = RotationManager::new(1);
let plain = url(None);
assert!(mgr.update(0, &plain));
assert!(mgr.update(0, &plain));
let single = url(Some(vec!["https://f0.test"]));
assert!(mgr.update(0, &single));
assert_eq!(mgr.front_str(0, &single), Some("f0.test"));
assert!(mgr.update(0, &single));
assert_eq!(mgr.front_str(0, &single), Some("f0.test"));
}
#[test]
fn update_cycles_through_fronts_and_reports_wraparound_only_on_the_last_one() {
let url = url(Some(vec![
"https://f0.test",
"https://f1.test",
"https://f2.test",
]));
let mgr = RotationManager::new(1);
assert!(!mgr.update(0, &url));
assert_eq!(mgr.front_str(0, &url), Some("f1.test"));
assert!(!mgr.update(0, &url));
assert_eq!(mgr.front_str(0, &url), Some("f2.test"));
assert!(mgr.update(0, &url));
assert_eq!(mgr.front_str(0, &url), Some("f0.test"));
}
#[test]
fn update_state_is_independent_per_host() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(2);
mgr.update(0, &url);
assert_eq!(mgr.front_str(0, &url), Some("f1.test"));
assert_eq!(mgr.front_str(1, &url), Some("f0.test"));
}
#[test]
fn take_rotation_turn_without_fronts_never_advances() {
let url = url(None);
let mgr = RotationManager::new(1);
for _ in 0..3 {
assert!(!mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), None);
}
}
#[test]
fn take_rotation_turn_with_empty_fronts_never_advances() {
let url = url(Some(Vec::new()));
let mgr = RotationManager::new(1);
for _ in 0..3 {
assert!(!mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), None);
}
}
#[test]
fn take_rotation_turn_lap_sequence() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(1);
let lap = |mgr: &RotationManager| {
[(); 3].map(|()| {
let advanced = mgr.take_rotation_turn(0, &url);
(advanced, mgr.active_rotation_front_str(0, &url))
})
};
let expected = [
(true, Some("f0.test")),
(true, Some("f1.test")),
(false, None),
];
assert_eq!(lap(&mgr), expected, "first lap");
assert_eq!(lap(&mgr), expected, "second lap");
}
#[test]
fn take_rotation_turn_single_front_alternates_with_direct() {
let url = url(Some(vec!["https://f0.test"]));
let mgr = RotationManager::new(1);
assert!(mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), Some("f0.test"));
assert!(!mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), None);
assert!(mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), Some("f0.test"));
}
#[test]
fn take_rotation_turn_state_is_independent_per_host() {
let url = url(Some(vec!["https://f0.test"]));
let mgr = RotationManager::new(2);
assert!(mgr.take_rotation_turn(0, &url));
assert_eq!(mgr.active_rotation_front_str(0, &url), Some("f0.test"));
assert_eq!(mgr.active_rotation_front_str(1, &url), None);
}
#[test]
fn update_after_take_rotation_turn_does_not_panic_and_stays_in_bounds() {
let url = url(Some(vec!["https://f0.test", "https://f1.test"]));
let mgr = RotationManager::new(1);
mgr.take_rotation_turn(0, &url);
mgr.take_rotation_turn(0, &url);
mgr.update(0, &url);
assert!(mgr.front_str(0, &url).is_some());
}
}