use std::num::NonZeroUsize;
use std::time::Duration;
use tokio::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostConcurrency {
max: NonZeroUsize,
adaptive: bool,
}
impl HostConcurrency {
#[must_use]
pub fn fixed(limit: usize) -> Self {
Self {
max: NonZeroUsize::new(limit).unwrap_or(NonZeroUsize::MIN),
adaptive: false,
}
}
#[must_use]
pub fn adaptive(max: usize) -> Self {
Self {
adaptive: true,
..Self::fixed(max)
}
}
pub(crate) const fn max(self) -> usize {
self.max.get()
}
pub(crate) const fn is_adaptive(self) -> bool {
self.adaptive
}
}
impl From<usize> for HostConcurrency {
fn from(limit: usize) -> Self {
Self::fixed(limit)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Feedback {
pub(crate) started: Instant,
pub(crate) latency: Option<Duration>,
pub(crate) unchanged: bool,
pub(crate) signal: Signal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Signal {
Ok,
Failed,
Throttled,
}
const SLOW: u32 = 2;
const DRIFT: u32 = 16;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Limit {
current: usize,
successes: usize,
best: Option<Duration>,
}
impl Limit {
pub(crate) const fn new() -> Self {
Self {
current: 1,
successes: 0,
best: None,
}
}
pub(crate) const fn get(&self) -> usize {
self.current
}
pub(crate) fn observe(&mut self, latency: Option<Duration>) -> bool {
let Some(latency) = latency else {
return false;
};
let slow = self.best.is_some_and(|best| latency > best * SLOW);
self.best = Some(match self.best {
Some(best) if latency > best => best + latency.saturating_sub(best) / DRIFT,
_ => latency,
});
slow
}
pub(crate) fn grow(&mut self, max: usize) {
self.successes += 1;
if self.successes >= self.current {
self.current = (self.current + 1).min(max);
self.successes = 0;
}
}
pub(crate) fn halve(&mut self) -> bool {
if self.current == 1 {
return false;
}
self.current /= 2;
self.successes = 0;
true
}
}
#[cfg(test)]
mod tests {
use super::*;
const fn ms(ms: u64) -> Duration {
Duration::from_millis(ms)
}
#[test]
fn grows_by_one_per_round_up_to_the_max() {
let mut limit = Limit::new();
let mut limits = Vec::new();
for _ in 0..12 {
limit.grow(4);
limits.push(limit.get());
}
assert_eq!(limits, [2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4]);
}
#[test]
fn halves_down_to_one() {
let mut limit = Limit::new();
for _ in 0..20 {
limit.grow(8);
}
assert_eq!(limit.get(), 6);
assert!(limit.halve());
assert_eq!(limit.get(), 3);
assert!(limit.halve());
assert!(!limit.halve(), "already one");
assert_eq!(limit.get(), 1);
}
#[test]
fn responses_twice_as_slow_as_the_best_are_slow() {
let mut limit = Limit::new();
assert!(!limit.observe(Some(ms(100))));
assert!(!limit.observe(Some(ms(180))));
assert_eq!(
limit.best,
Some(ms(105)),
"the best follows slower times slowly"
);
assert!(limit.observe(Some(ms(300))));
assert!(!limit.observe(Some(ms(50))));
assert_eq!(limit.best, Some(ms(50)));
assert!(!limit.observe(None));
}
#[test]
fn limits_are_at_least_one() {
assert_eq!(HostConcurrency::fixed(0).max(), 1);
assert_eq!(HostConcurrency::from(3), HostConcurrency::fixed(3));
assert!(HostConcurrency::adaptive(0).is_adaptive());
}
}