use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostDelay {
Fixed(Duration),
Adaptive(AdaptiveDelay),
}
impl HostDelay {
#[must_use]
pub const fn adaptive() -> AdaptiveDelay {
AdaptiveDelay::new()
}
pub(crate) fn initial(self) -> Duration {
match self {
Self::Fixed(delay) => delay,
Self::Adaptive(adaptive) => adaptive.start(),
}
}
pub(crate) fn is_zero(self) -> bool {
matches!(self, Self::Fixed(delay) if delay.is_zero())
}
}
impl Default for HostDelay {
fn default() -> Self {
Self::Fixed(Duration::ZERO)
}
}
impl From<Duration> for HostDelay {
fn from(delay: Duration) -> Self {
Self::Fixed(delay)
}
}
impl From<AdaptiveDelay> for HostDelay {
fn from(adaptive: AdaptiveDelay) -> Self {
Self::Adaptive(adaptive)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdaptiveDelay {
target_concurrency: u32,
start: Duration,
min: Duration,
max: Duration,
}
impl AdaptiveDelay {
#[must_use]
pub const fn new() -> Self {
Self {
target_concurrency: 1,
start: Duration::from_secs(1),
min: Duration::ZERO,
max: Duration::from_mins(1),
}
}
#[must_use]
pub fn with_target_concurrency(mut self, concurrency: usize) -> Self {
self.target_concurrency = u32::try_from(concurrency.max(1)).unwrap_or(u32::MAX);
self
}
#[must_use]
pub const fn with_start(mut self, delay: Duration) -> Self {
self.start = delay;
self
}
#[must_use]
pub const fn with_bounds(mut self, min: Duration, max: Duration) -> Self {
if min.as_nanos() <= max.as_nanos() {
self.min = min;
self.max = max;
} else {
self.min = max;
self.max = min;
}
self
}
pub(crate) fn next(&self, current: Duration, feedback: Feedback) -> Duration {
let next = match (feedback.signal, feedback.latency) {
(Signal::Throttled, _) => (current * 2).max(self.start),
(signal, Some(latency)) => {
let target = latency / self.target_concurrency;
let average = (current + target) / 2;
match signal {
Signal::Failed => average.max(current),
Signal::Ok | Signal::Throttled => average,
}
}
(_, None) => current,
};
next.clamp(self.min, self.max)
}
pub(crate) fn start(&self) -> Duration {
self.start.clamp(self.min, self.max)
}
}
impl Default for AdaptiveDelay {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Feedback {
pub(crate) latency: Option<Duration>,
pub(crate) signal: Signal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Signal {
Ok,
Failed,
Throttled,
}
#[cfg(test)]
mod tests {
use super::*;
fn feedback(signal: Signal, latency_ms: u64) -> Feedback {
Feedback {
latency: Some(Duration::from_millis(latency_ms)),
signal,
}
}
#[test]
fn moves_halfway_towards_the_latency_over_the_target() {
let delay = AdaptiveDelay::new().with_target_concurrency(2);
let current = Duration::from_secs(1);
let next = delay.next(current, feedback(Signal::Ok, 400));
assert_eq!(next, Duration::from_millis(600));
let next = delay.next(Duration::from_millis(100), feedback(Signal::Ok, 900));
assert_eq!(next, Duration::from_millis(275));
}
#[test]
fn failures_never_lower_the_delay() {
let delay = AdaptiveDelay::new();
let current = Duration::from_secs(1);
assert_eq!(delay.next(current, feedback(Signal::Failed, 10)), current);
let slower = delay.next(current, feedback(Signal::Failed, 3000));
assert_eq!(slower, Duration::from_secs(2));
}
#[test]
fn throttling_doubles_the_delay_within_bounds() {
let delay = AdaptiveDelay::new().with_bounds(Duration::ZERO, Duration::from_secs(5));
let throttled = Feedback {
latency: None,
signal: Signal::Throttled,
};
assert_eq!(
delay.next(Duration::from_secs(2), throttled),
Duration::from_secs(4)
);
assert_eq!(
delay.next(Duration::from_secs(4), throttled),
Duration::from_secs(5)
);
assert_eq!(
delay.next(Duration::ZERO, throttled),
Duration::from_secs(1)
);
}
#[test]
fn bounds_and_missing_latencies() {
let delay = AdaptiveDelay::new()
.with_bounds(Duration::from_secs(1), Duration::from_millis(500))
.with_start(Duration::ZERO);
assert_eq!(delay.start(), Duration::from_millis(500));
let next = delay.next(Duration::from_millis(700), feedback(Signal::Ok, 0));
assert_eq!(next, Duration::from_millis(500));
let none = Feedback {
latency: None,
signal: Signal::Ok,
};
assert_eq!(
delay.next(Duration::from_millis(700), none),
Duration::from_millis(700)
);
}
#[test]
fn target_concurrency_is_at_least_one() {
let delay = AdaptiveDelay::new().with_target_concurrency(0);
assert_eq!(delay, AdaptiveDelay::new());
}
}