use std::sync::Mutex;
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;
use dig_nat::{PeerId, TraversalKind};
use crate::config::SelectorConfig;
use crate::observe::{PeerSnapshot, SelectorSnapshot};
use crate::pool_event::{PoolEvent, PoolRemovalReason};
use crate::quality::PeerQuality;
use crate::registry::Registry;
use crate::scoring::{score_peer, PeerClass, RelayModel, SaturationModel, ScoredPeer};
use crate::types::{
Candidate, ContentRequest, OutcomeKind, OutcomeResult, Provenance, RangePlanDelta,
SelectedPeer, Selection, TransferOutcome,
};
struct Inner {
registry: Registry,
saturation: SaturationModel,
relay: RelayModel,
rng: Pcg64Mcg,
dispatched: std::collections::HashMap<(PeerId, usize), DispatchRecord>,
epoch: u64,
last_selected: std::collections::HashMap<PeerId, u64>,
}
impl Inner {
fn prune_evicted_side_maps(&mut self) {
for peer in self.registry.drain_evicted() {
self.last_selected.remove(&peer);
self.dispatched.retain(|(id, _), _| *id != peer);
}
}
}
#[derive(Clone, Copy)]
struct DispatchRecord {
in_flight_at_dispatch: u32,
class: PeerClass,
}
pub struct PeerSelector {
config: SelectorConfig,
inner: Mutex<Inner>,
}
impl PeerSelector {
pub fn new(config: SelectorConfig) -> Self {
let rng = Pcg64Mcg::seed_from_u64(config.effective_seed());
let registry = Registry::new(config.registry_capacity);
PeerSelector {
config,
inner: Mutex::new(Inner {
registry,
saturation: SaturationModel::default(),
relay: RelayModel::default(),
rng,
dispatched: std::collections::HashMap::new(),
epoch: 0,
last_selected: std::collections::HashMap::new(),
}),
}
}
fn now(&self) -> u64 {
self.config.clock.now()
}
pub fn select(&self, req: &ContentRequest, candidates: &[Candidate]) -> Selection {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
for c in candidates {
inner.registry.upsert_candidate(c, Provenance::Dht, now);
}
inner.prune_evicted_side_maps();
self.select_over(&mut inner, req, &candidate_ids(candidates), &[], now)
}
pub fn rebalance(
&self,
req: &ContentRequest,
active: &[PeerId],
need: &RangePlanDelta,
) -> Selection {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
let want = need.len().max(1);
let effective = ContentRequest {
parallelism: req.effective_parallelism().min(want).max(1),
..req.clone()
};
let pool: Vec<PeerId> = inner
.registry
.iter()
.filter(|e| e.is_eligible())
.map(|e| e.peer_id)
.collect();
self.select_over(&mut inner, &effective, &pool, active, now)
}
fn select_over(
&self,
inner: &mut Inner,
req: &ContentRequest,
pool: &[PeerId],
deranked: &[PeerId],
now: u64,
) -> Selection {
inner.registry.reclaim_stale_in_flight(now);
if pool.is_empty() {
return Selection::empty();
}
let mut scored: Vec<(PeerId, ScoredPeer)> = Vec::with_capacity(pool.len());
for id in pool {
let Some(entry) = inner.registry.get(id) else {
continue;
};
if !entry.is_eligible() {
continue;
}
scored.push((*id, score_peer(entry, &inner.saturation, &inner.relay, 0.0)));
}
if scored.is_empty() {
return Selection::empty();
}
let (worst_proven, best_proven) = proven_score_bounds(&scored);
let exploration_bonus = if best_proven > worst_proven {
worst_proven + 0.25 * (best_proven - worst_proven)
} else if best_proven > 0.0 {
best_proven * 0.5
} else {
0.0
};
for (id, s) in &mut scored {
if s.exploratory {
s.effective_score = exploration_bonus;
}
if deranked.contains(id) {
s.effective_score *= DERANK_FACTOR;
s.headroom = s.headroom.saturating_sub(1);
}
}
inner.epoch = inner.epoch.wrapping_add(1);
let epoch = inner.epoch;
let want = req.effective_parallelism();
let explore_cap = explore_slots(want);
let last_sel: Vec<u64> = scored
.iter()
.map(|(id, _)| inner.last_selected.get(id).copied().unwrap_or(0))
.collect();
let jitter: Vec<u64> = scored.iter().map(|_| inner.rng.gen::<u64>()).collect();
let mut order: Vec<usize> = (0..scored.len()).collect();
order.sort_by(|&a, &b| {
let (_, sa) = &scored[a];
let (_, sb) = &scored[b];
sb.effective_score
.partial_cmp(&sa.effective_score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
if sa.exploratory && sb.exploratory {
last_sel[a]
.cmp(&last_sel[b])
.then_with(|| jitter[a].cmp(&jitter[b]))
} else {
sa.tie_break.cmp(&sb.tie_break)
}
})
});
let eligible = scored.len().max(1);
let starve_threshold = eligible as u64; let forced: Option<usize> = if want < eligible {
scored
.iter()
.enumerate()
.filter(|(idx, (_, s))| {
s.effective_score > -1.0e11 && {
let staleness = epoch.saturating_sub(last_sel[*idx]);
staleness >= starve_threshold
}
})
.max_by(|(ia, (_, sa)), (ib, (_, sb))| {
let stale_a = epoch.saturating_sub(last_sel[*ia]);
let stale_b = epoch.saturating_sub(last_sel[*ib]);
stale_a
.cmp(&stale_b)
.then_with(|| sb.tie_break.cmp(&sa.tie_break)) })
.map(|(idx, _)| idx)
} else {
None
};
let cap_pass1 = if forced.is_some() {
want.saturating_sub(1)
} else {
want
};
let forced_is_exploratory = forced.map(|i| scored[i].1.exploratory).unwrap_or(false);
let explore_budget_pass1 = if forced_is_exploratory {
explore_cap.saturating_sub(1)
} else {
explore_cap
};
let mut chosen: Vec<usize> = Vec::with_capacity(want);
let mut explore_used = 0usize;
for &i in &order {
if chosen.len() >= cap_pass1 {
break;
}
if Some(i) == forced {
continue; }
let (_, s) = &scored[i];
if s.exploratory {
if explore_used >= explore_budget_pass1 {
continue;
}
explore_used += 1;
}
chosen.push(i);
}
if let Some(fi) = forced {
if chosen.len() < want && !chosen.contains(&fi) {
chosen.push(fi);
}
}
let mut selection = Selection::empty();
for (rank, &i) in chosen.iter().enumerate() {
let (peer_id, s) = scored[i];
inner.last_selected.insert(peer_id, epoch);
let exploratory = s.exploratory || Some(i) == forced;
selection.peers.push(SelectedPeer {
peer_id,
rank: rank as u32,
max_concurrency: s.headroom.max(1),
exploratory,
});
}
for sp in &selection.peers {
let class = inner
.registry
.get(&sp.peer_id)
.map(|e| PeerClass::of(e.connection_class))
.unwrap_or(PeerClass::Unknown);
let in_flight_at_dispatch = inner
.registry
.get(&sp.peer_id)
.map(|e| e.quality.in_flight)
.unwrap_or(0);
inner
.registry
.add_in_flight(&sp.peer_id, sp.max_concurrency, now);
inner.dispatched.insert(
(sp.peer_id, usize::MAX),
DispatchRecord {
in_flight_at_dispatch,
class,
},
);
}
selection
}
pub fn record_outcome(&self, outcome: &TransferOutcome) {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
if inner.registry.get(&outcome.peer_id).is_none() {
let cand = Candidate::new(outcome.peer_id, Vec::new());
inner.registry.upsert_candidate(&cand, Provenance::Nat, now);
inner.prune_evicted_side_maps();
}
let range_index = match outcome.kind {
OutcomeKind::Range { index, .. } => index,
OutcomeKind::Request { .. } => usize::MAX,
};
let dispatch = inner
.dispatched
.remove(&(outcome.peer_id, range_index))
.or_else(|| inner.dispatched.remove(&(outcome.peer_id, usize::MAX)));
let class = dispatch.map(|d| d.class).unwrap_or_else(|| {
inner
.registry
.get(&outcome.peer_id)
.map(|e| PeerClass::of(e.connection_class))
.unwrap_or(PeerClass::Unknown)
});
let in_flight_at_dispatch =
dispatch
.map(|d| d.in_flight_at_dispatch)
.unwrap_or_else(|| {
inner
.registry
.get(&outcome.peer_id)
.map(|e| e.quality.in_flight)
.unwrap_or(1)
});
let throughput = outcome.throughput_bps();
if let (Some(bps), true) = (throughput, outcome.is_success()) {
inner.saturation.observe(class, in_flight_at_dispatch, bps);
inner.relay.observe(class.is_relayed(), bps);
}
if let Some(entry) = inner.registry.get_mut(&outcome.peer_id) {
apply_outcome_to_quality(&mut entry.quality, outcome, throughput);
entry.quality.bump_samples();
entry.last_outcome_at = Some(outcome.at.max(now));
}
inner.registry.release_in_flight(&outcome.peer_id, 1);
}
pub fn on_pool_event(&self, event: &PoolEvent) {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
match event {
PoolEvent::PeerAdded { peer_id, .. } => {
inner
.registry
.mark_connected(*peer_id, Provenance::Gossip, now);
inner.prune_evicted_side_maps();
}
PoolEvent::PeerRemoved { peer_id, reason } => {
let banned = matches!(reason, PoolRemovalReason::Banned);
inner.registry.mark_disconnected(peer_id, banned);
}
}
}
pub fn on_connection_class(&self, peer: &PeerId, class: TraversalKind) {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
inner.registry.set_connection_class(*peer, class, now);
inner.prune_evicted_side_maps();
}
pub fn upsert_candidate(&self, candidate: &Candidate) {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
inner
.registry
.upsert_candidate(candidate, Provenance::Manual, now);
inner.prune_evicted_side_maps();
}
pub fn remove_peer(&self, peer: &PeerId) {
let mut inner = self.inner.lock().expect("selector mutex poisoned");
inner.registry.remove(peer);
inner.prune_evicted_side_maps();
}
pub fn on_dispatch(&self, peer: &PeerId, count: u32) {
let now = self.now();
let mut inner = self.inner.lock().expect("selector mutex poisoned");
inner.registry.add_in_flight(peer, count, now);
}
pub fn peer_snapshot(&self, peer: &PeerId) -> Option<PeerSnapshot> {
let inner = self.inner.lock().expect("selector mutex poisoned");
inner.registry.get(peer).map(PeerSnapshot::of)
}
pub fn snapshot(&self) -> SelectorSnapshot {
let inner = self.inner.lock().expect("selector mutex poisoned");
SelectorSnapshot::build(
inner.registry.iter(),
&inner.saturation,
&inner.relay,
inner.last_selected.len(),
inner.dispatched.len(),
)
}
pub fn registry_size(&self) -> usize {
let inner = self.inner.lock().expect("selector mutex poisoned");
inner.registry.len()
}
}
const DERANK_FACTOR: f64 = 0.5;
fn explore_slots(want: usize) -> usize {
want.div_ceil(3).max(1)
}
fn proven_score_bounds(scored: &[(PeerId, ScoredPeer)]) -> (f64, f64) {
let mut worst = f64::INFINITY;
let mut best = f64::NEG_INFINITY;
let mut any = false;
for (_, s) in scored {
if s.exploratory {
continue; }
if s.effective_score <= -1.0e11 {
continue; }
any = true;
worst = worst.min(s.effective_score);
best = best.max(s.effective_score);
}
if any {
(worst.max(0.0), best.max(0.0))
} else {
(0.0, 0.0)
}
}
fn apply_outcome_to_quality(
quality: &mut PeerQuality,
outcome: &TransferOutcome,
throughput: Option<f64>,
) {
match outcome.result {
OutcomeResult::Success => {
if let Some(bps) = throughput {
quality.observe_throughput(bps);
}
if let Some(rtt) = outcome.rtt_ms {
quality.observe_rtt(rtt as f64);
}
quality.observe_result(true, false);
}
OutcomeResult::Failure { reason } => {
if reason.blames_peer() {
quality.observe_result(false, reason.is_hard());
}
}
OutcomeResult::Interrupted { .. } => {
quality.observe_result(false, false);
}
}
}
fn candidate_ids(candidates: &[Candidate]) -> Vec<PeerId> {
candidates.iter().map(|c| c.peer_id).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::SelectorConfig;
use crate::scoring::{SCORE_PEER_CALLS, SCORE_PEER_CALLS_LOCK};
use crate::types::{Candidate, OutcomeKind, OutcomeResult, TransferOutcome};
use dig_dht::{CandidateAddr, ContentId};
use std::sync::atomic::Ordering;
fn pid(b: u8) -> PeerId {
PeerId::from_bytes([b; 32])
}
fn candidate(b: u8) -> Candidate {
Candidate::new(pid(b), vec![CandidateAddr::direct("10.0.0.1", 9444)])
}
fn content() -> ContentId {
ContentId::store([0x77; 32])
}
#[test]
fn select_scores_each_pooled_peer_at_most_once() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 5));
for b in 0..5u8 {
for i in 0..3u64 {
sel.record_outcome(&TransferOutcome {
peer_id: pid(b),
content: content(),
kind: OutcomeKind::Range {
index: i as usize,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 100_000,
duration_ms: 1000,
rtt_ms: Some(10),
at: 1000 + i,
});
}
}
let candidates: Vec<Candidate> = (0..7u8).map(candidate).collect();
let _guard = SCORE_PEER_CALLS_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let before = SCORE_PEER_CALLS.load(Ordering::SeqCst);
let _ = sel.select(&ContentRequest::new(content(), 3), &candidates);
let calls = SCORE_PEER_CALLS.load(Ordering::SeqCst) - before;
assert!(
calls <= candidates.len() as u64,
"score_peer must run at most once per pooled peer ({} peers), got {calls} calls \
(a separate proven-bounds pre-pass would double-count the non-cold ones)",
candidates.len()
);
}
}