Fix rounding bug in token_bucket for ~1 or 2 second buckets

This commit is contained in:
Mingwei Samuel 2021-07-23 18:03:55 -07:00
parent f090d107b2
commit 80eeb184a0
3 changed files with 52 additions and 43 deletions

View file

@ -68,9 +68,7 @@ impl VectorTokenBucket {
// Effective duration.
let d_eff = duration + duration_overhead;
let burst_duration = Duration::new(
(d_eff.as_secs() as f32 * burst_pct).ceil() as u64,
(d_eff.subsec_nanos() as f32 * burst_pct).ceil() as u32);
let burst_duration = d_eff.mul_f32(burst_pct);
let burst_limit = std::cmp::max(1,
(total_limit as f32 * burst_pct).floor() as usize);
debug_assert!(burst_limit <= total_limit);
@ -137,7 +135,21 @@ impl TokenBucket for VectorTokenBucket {
for _ in 0..n {
timestamps.push_front(now);
}
timestamps.len() <= self.total_limit
// Check total limit.
if self.total_limit < timestamps.len() {
return false;
}
// Check burst limit.
if let Some(burst_time) = timestamps.get(self.burst_limit) {
let duration_since = now.duration_since(*burst_time); // `now` before `burst_time` will panic.
if duration_since < self.burst_duration {
return false;
}
}
return true;
}
fn get_bucket_duration(&self) -> Duration {

View file

@ -32,46 +32,42 @@ mod token_bucket {
assert_eq!(1, bucket.burst_limit);
}
#[test]
fn test_saturated_100_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1.00);
Instant::set_time(50_000);
assert!(bucket.get_tokens(100), "All tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(1001); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(100), "All tokens should be available after a bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_95_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.50);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
Instant::set_time(50_000);
assert!(bucket.get_tokens(95), "95 tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(475); // Total 951.
Instant::advance_time(475);
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(476); // Total 951. Extra buffer for Duration(0).
Instant::advance_time(476); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(51);
assert!(bucket.get_tokens(95), "95 tokens should be available.");
Instant::advance_time(51); // Total 1002.
assert!(bucket.get_tokens(90), "90 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(951);
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert!(bucket.get_tokens(10), "Last 10 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_violated_50_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.50);
Instant::set_time(50_000);
assert!(!bucket.get_tokens(90), "Burst should be violated.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_50_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.5);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.50);
Instant::set_time(50_000);
assert!(bucket.get_tokens(50), "Half the tokens should be immediately available.");
@ -93,18 +89,18 @@ mod token_bucket {
#[test]
fn test_many() {
Instant::set_time(50_000);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_eq!(None, bucket.get_delay(), "Should not be blocked.");
for _ in 0..20_000 {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.5);
assert!(bucket.get_tokens(50), "Should have not violated limit. i=-1.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay. i=-1.");
for i in 0..20_000 {
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
assert!(bucket.get_tokens(50), "Should have not violated limit. i={}.", i);
assert_ne!(None, bucket.get_delay(), "Bucket should have delay. i={}.", i);
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
assert!(bucket.get_tokens(50), "Should have not violated limit. i={}.", i);
assert_ne!(None, bucket.get_delay(), "Bucket should have delay. i={}.", i);
}
assert!(bucket.timestamps.lock().len() < 110, "Check memory leak.");
assert!(bucket.timestamps.lock().len() < 110, "Should not memory leak.");
}
}
}

View file

@ -47,16 +47,17 @@ async_tests!{
Ok(())
},
// tft-league-v1.getLeagueEntriesForSummoner
// https://github.com/MingweiSamuel/Riven/issues/25
tft_league_getleagueentriesforsummoner: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(Region::JP, "Caihonbbt");
let sr = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"Caihonbbt\"".to_owned())?;
let lp = RIOT_API.tft_league_v1().get_league_entries_for_summoner(Region::JP, &sr.id);
let lr = lp.await.map_err(|e| e.to_string())?;
rassert!(0 < lr.len());
Ok(())
},
// Disabled: Caihonbbt no longer ranked.
// // tft-league-v1.getLeagueEntriesForSummoner
// // https://github.com/MingweiSamuel/Riven/issues/25
// tft_league_getleagueentriesforsummoner: async {
// let sp = RIOT_API.summoner_v4().get_by_summoner_name(Region::JP, "Caihonbbt");
// let sr = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"Caihonbbt\"".to_owned())?;
// let lp = RIOT_API.tft_league_v1().get_league_entries_for_summoner(Region::JP, &sr.id);
// let lr = lp.await.map_err(|e| e.to_string())?;
// rassert!(0 < lr.len());
// Ok(())
// },
// tft-league-v1.getTopRatedLadder
// https://github.com/MingweiSamuel/Riven/issues/24
tft_league_gettopratedladder: async {