use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandidateRef {
pub peer_id: String,
pub addrs: Vec<String>,
pub tag: Option<u64>,
}
impl CandidateRef {
pub fn new(peer_id: impl Into<String>, addrs: Vec<String>) -> Self {
CandidateRef {
peer_id: peer_id.into(),
addrs,
tag: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SelectRequest<'a> {
pub content_key: &'a str,
pub candidates: &'a [CandidateRef],
pub ranges_needed: usize,
pub inflight: usize,
}
#[derive(Debug, Clone, Default)]
pub struct SelectPlan {
pub ordered: Vec<String>,
pub assignments: Vec<(usize, String)>,
}
impl SelectPlan {
pub fn ordered(peers: Vec<String>) -> Self {
SelectPlan {
ordered: peers,
assignments: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeOutcome {
pub peer_id: String,
pub bytes: u64,
pub elapsed: Duration,
pub result: RangeResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RangeResult {
Ok,
Failed,
TimedOut,
}
pub trait SourceSelector: Send + Sync {
fn select(&self, req: &SelectRequest) -> SelectPlan;
fn record(&self, outcome: &RangeOutcome);
}
#[derive(Debug, Default)]
pub struct NullSelector {
cursor: AtomicUsize,
}
impl NullSelector {
pub fn new() -> Self {
NullSelector::default()
}
}
impl SourceSelector for NullSelector {
fn select(&self, req: &SelectRequest) -> SelectPlan {
let n = req.candidates.len();
if n == 0 {
return SelectPlan::default();
}
let start = self.cursor.fetch_add(1, Ordering::Relaxed) % n;
let ordered = req
.candidates
.iter()
.cycle()
.skip(start)
.take(n)
.map(|c| c.peer_id.clone())
.collect();
SelectPlan::ordered(ordered)
}
fn record(&self, _outcome: &RangeOutcome) {}
}
#[cfg(test)]
mod tests {
use super::*;
fn candidates(n: usize) -> Vec<CandidateRef> {
(0..n)
.map(|i| CandidateRef::new(format!("peer{i}"), vec![format!("10.0.0.{i}:9444")]))
.collect()
}
fn request(cands: &[CandidateRef]) -> SelectRequest<'_> {
SelectRequest {
content_key: "test-content-key",
candidates: cands,
ranges_needed: 4,
inflight: 0,
}
}
#[test]
fn null_selector_returns_all_candidates_in_a_rotating_order() {
let sel = NullSelector::new();
let cands = candidates(3);
let p0 = sel.select(&request(&cands));
let p1 = sel.select(&request(&cands));
assert_eq!(p0.ordered.len(), 3);
assert_eq!(p1.ordered.len(), 3);
assert_ne!(p0.ordered[0], p1.ordered[0], "start rotates each pass");
let mut sorted = p0.ordered.clone();
sorted.sort();
assert_eq!(sorted, vec!["peer0", "peer1", "peer2"]);
}
#[test]
fn null_selector_empty_candidates_is_empty_plan() {
let sel = NullSelector::new();
let cands = candidates(0);
let plan = sel.select(&request(&cands));
assert!(plan.ordered.is_empty());
assert!(plan.assignments.is_empty());
}
#[test]
fn null_selector_record_is_a_noop() {
let sel = NullSelector::new();
sel.record(&RangeOutcome {
peer_id: "peer0".into(),
bytes: 1024,
elapsed: Duration::from_millis(5),
result: RangeResult::Ok,
});
let cands = candidates(2);
assert_eq!(sel.select(&request(&cands)).ordered.len(), 2);
}
#[test]
fn select_plan_ordered_helper_has_no_assignments() {
let plan = SelectPlan::ordered(vec!["a".into(), "b".into()]);
assert_eq!(plan.ordered, vec!["a", "b"]);
assert!(plan.assignments.is_empty());
}
}