-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f0a91ec
commit 852e74e
Showing
5 changed files
with
202 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
use std::cell::RefCell; | ||
use std::cmp::Ordering; | ||
use std::collections::HashMap; | ||
use std::sync::{Arc, Mutex, MutexGuard}; | ||
use std::time::{Duration, SystemTime}; | ||
|
||
use anyhow::Result; | ||
use rand::Rng; | ||
|
||
pub const HOUR: Duration = Duration::from_secs(3600); | ||
#[derive(Debug, Clone)] | ||
struct RelayStats { | ||
pub failure_times: Vec<SystemTime>, | ||
} | ||
|
||
impl RelayStats { | ||
fn new() -> RelayStats { | ||
RelayStats { | ||
failure_times: Vec::new(), | ||
} | ||
} | ||
|
||
fn _clean(&mut self, retain: Duration) { | ||
let count = self.failure_times.len(); | ||
self.failure_times.retain(|t| { | ||
t.elapsed().unwrap_or({ | ||
log::warn!("the system experience system time error, ranker may malfunction."); | ||
retain.saturating_add(Duration::from_secs(1)) | ||
}) < retain | ||
}); | ||
log::trace!("cleaning {:?} entires", count - &self.failure_times.len()); | ||
} | ||
|
||
fn add_failure(&mut self, retain: Duration) { | ||
self.failure_times.push(SystemTime::now()); | ||
self._clean(retain); | ||
} | ||
|
||
fn failures_last(&self, period: Duration) -> Result<usize> { | ||
let mut count = 0; | ||
for failure_time in &self.failure_times { | ||
if failure_time | ||
.elapsed() | ||
.map_err(|e| anyhow::anyhow!(e.to_string()))? | ||
< period | ||
{ | ||
break; | ||
} | ||
count += 1; | ||
} | ||
Ok(&self.failure_times.len() - count) | ||
} | ||
|
||
fn mean_failure_rate(&self, period: Duration) -> Result<f64> { | ||
let failures = self.failures_last(period)?; | ||
|
||
if failures == 0 { | ||
return Ok(0.0); | ||
} | ||
Ok(failures as f64 / (period.as_secs_f64() / 3600.0)) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct RelayRanker { | ||
domain_stats: Arc<Mutex<RefCell<HashMap<String, RelayStats>>>>, | ||
max_duration: Duration, | ||
} | ||
|
||
impl RelayRanker { | ||
pub fn new(retain: Duration) -> RelayRanker { | ||
RelayRanker { | ||
domain_stats: Arc::new(Mutex::new(RefCell::new(HashMap::new()))), | ||
max_duration: retain, | ||
} | ||
} | ||
|
||
pub fn downvote(&self, domain: impl Into<String>) -> Result<()> { | ||
let guard = match self.domain_stats.lock() { | ||
Ok(guard) => guard, | ||
Err(_) => return Err(anyhow::anyhow!("failed to acquire lock")), | ||
}; | ||
let mut inner = guard.borrow_mut(); | ||
let stats = inner.entry(domain.into()).or_insert(RelayStats::new()); | ||
stats.add_failure(self.max_duration); | ||
Ok(()) | ||
} | ||
|
||
pub fn reorder(&self, domains: &mut Vec<&str>) -> Result<()> { | ||
let guard = match self.domain_stats.lock() { | ||
Ok(guard) => guard, | ||
Err(_) => return Err(anyhow::anyhow!("failed to acquire lock")), | ||
}; | ||
|
||
domains.sort_by(|a, b| { | ||
let a_failure_rate = self._mean_failure_rate(*a, &guard).unwrap_or_default(); | ||
let b_failure_rate = self._mean_failure_rate(*b, &guard).unwrap_or_default(); | ||
if a_failure_rate == b_failure_rate { | ||
let mut rng = rand::thread_rng(); | ||
rng.gen::<bool>().cmp(&rng.gen::<bool>()) | ||
} else { | ||
a_failure_rate | ||
.partial_cmp(&b_failure_rate) | ||
.unwrap_or(Ordering::Equal) | ||
} | ||
}); | ||
log::debug!("ranking system hint: {:?}", domains); | ||
Ok(()) | ||
} | ||
|
||
fn _mean_failure_rate( | ||
&self, | ||
domain: impl Into<String>, | ||
guard: &MutexGuard<'_, RefCell<HashMap<String, RelayStats>>>, | ||
) -> Result<f64> { | ||
let inner = guard.borrow(); | ||
if let Some(stats) = inner.get(&domain.into()) { | ||
stats.mean_failure_rate(self.max_duration) | ||
} else { | ||
Ok(0.0) | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn test_downvoted_at_last() { | ||
let ranking_system = RelayRanker::new(HOUR); | ||
|
||
let _ = ranking_system.downvote("bing.com"); | ||
let mut domains = vec!["example.com", "bing.com", "google.com"]; | ||
let _ = ranking_system.reorder(&mut domains); | ||
|
||
assert_eq!(*domains.last().unwrap(), "bing.com"); | ||
} | ||
|
||
#[test] | ||
fn test_order_by_failure_rate() { | ||
let ranking_system = RelayRanker::new(HOUR); | ||
|
||
let _ = ranking_system.downvote("bing.com"); | ||
let _ = ranking_system.downvote("example.com"); | ||
let _ = ranking_system.downvote("example.com"); | ||
|
||
let mut domains = vec!["example.com", "bing.com", "google.com"]; | ||
let _ = ranking_system.reorder(&mut domains); | ||
|
||
assert_eq!(domains, vec!("google.com", "bing.com", "example.com")); | ||
} | ||
|
||
#[test] | ||
fn test_rank_healing() { | ||
let ranking_system = RelayRanker::new(HOUR); | ||
|
||
let _ = ranking_system.downvote("bing.com"); | ||
|
||
let binding = ranking_system.domain_stats.lock().unwrap(); | ||
let mut inner = binding.borrow_mut(); | ||
let ds = inner.get_mut("bing.com").unwrap(); | ||
if let Some(first) = ds.failure_times.get_mut(0) { | ||
*first = SystemTime::checked_sub(&SystemTime::now(), HOUR * 2).unwrap(); | ||
} | ||
let failure_rate = ds.mean_failure_rate(HOUR).unwrap(); | ||
assert_eq!(failure_rate, 0.0); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters