use std::time::Duration;
use exocore_core::{
time::{Clock, Instant},
utils::backoff::{BackoffCalculator, BackoffConfig},
};
pub struct RequestTracker {
clock: Clock,
backoff_calculator: BackoffCalculator,
last_request_send: Option<Instant>,
last_response_receive: Option<Instant>,
force_next_request: Option<bool>,
}
impl RequestTracker {
pub fn new_with_clock(clock: Clock, config: RequestTrackerConfig) -> RequestTracker {
let backoff_calculator = BackoffCalculator::new(
clock.clone(),
BackoffConfig {
normal_constant: config.min_interval,
failure_constant: config.min_interval,
failure_exp_base: 2.0,
failure_exp_multiplier: Duration::from_secs(5),
failure_maximum: config.max_interval,
},
);
RequestTracker {
clock,
backoff_calculator,
last_request_send: None,
last_response_receive: None,
force_next_request: None,
}
}
pub fn set_last_send_now(&mut self) {
self.last_request_send = Some(self.clock.instant());
}
pub fn set_last_responded_now(&mut self) {
self.last_response_receive = Some(self.clock.instant());
self.backoff_calculator.reset();
}
pub fn can_send_request(&mut self) -> bool {
if let Some(_force) = self.force_next_request.take() {
return true;
}
let should_send_request = self.last_request_send.map_or(true, |send_time| {
(self.clock.instant() - send_time) >= self.backoff_calculator.backoff_duration()
});
if should_send_request {
if self.last_request_send.is_some() && !self.has_responded_last_request() {
self.backoff_calculator.increment_failure();
}
true
} else {
false
}
}
pub fn force_next_request(&mut self) {
self.force_next_request = Some(true);
}
pub fn reset(&mut self) {
self.last_request_send = None;
self.last_response_receive = None;
self.force_next_request = None;
}
fn has_responded_last_request(&self) -> bool {
matches!((self.last_request_send, self.last_response_receive), (Some(send), Some(resp)) if resp > send)
}
pub fn response_failure_count(&self) -> usize {
self.backoff_calculator.consecutive_failures_count() as usize
}
#[cfg(test)]
pub fn set_response_failure_count(&mut self, count: usize) {
for _ in 0..count {
self.backoff_calculator.increment_failure();
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct RequestTrackerConfig {
pub min_interval: Duration,
pub max_interval: Duration,
}
impl Default for RequestTrackerConfig {
fn default() -> Self {
RequestTrackerConfig {
min_interval: Duration::from_secs(5),
max_interval: Duration::from_secs(30),
}
}
}
#[cfg(test)]
mod tests {
use exocore_core::time::Clock;
use super::*;
#[test]
fn test_can_send_request_interval() {
let mock_clock = Clock::new_mocked();
let mut tracker =
RequestTracker::new_with_clock(mock_clock.clone(), RequestTrackerConfig::default());
assert!(tracker.can_send_request());
tracker.set_last_send_now();
assert!(!tracker.can_send_request());
assert!(!tracker.has_responded_last_request());
mock_clock.set_fixed_instant(Instant::now() + Duration::from_millis(5001));
assert!(tracker.can_send_request());
assert!(!tracker.has_responded_last_request());
assert_eq!(tracker.response_failure_count(), 1);
}
#[test]
fn test_force_request() {
let mock_clock = Clock::new_mocked();
let mut tracker =
RequestTracker::new_with_clock(mock_clock, RequestTrackerConfig::default());
tracker.can_send_request();
tracker.set_last_send_now();
assert!(!tracker.can_send_request());
tracker.force_next_request();
assert!(tracker.can_send_request());
}
#[test]
fn test_reset() {
let mock_clock = Clock::new_mocked();
let mut tracker =
RequestTracker::new_with_clock(mock_clock, RequestTrackerConfig::default());
tracker.can_send_request();
tracker.set_last_send_now();
assert!(!tracker.can_send_request());
tracker.reset();
assert!(tracker.can_send_request());
}
}