use indexmap::IndexSet;
use kitsune_p2p_types::KAgent;
use std::{default, ops::Deref};
use crate::{backoff::FetchBackoff, FetchConfig};
const NUM_PROBE_ATTEMPTS: u32 = 10;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FetchSource {
Agent(KAgent),
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Sources {
inner: IndexSet<FetchSource>,
index: usize,
}
impl Deref for Sources {
type Target = IndexSet<FetchSource>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Sources {
pub(crate) fn new(queue: impl IntoIterator<Item = FetchSource>) -> Self {
Self {
inner: queue.into_iter().collect(),
index: 0,
}
}
pub(crate) fn next(
&mut self,
mut state_filter: impl FnMut(&FetchSource) -> bool,
) -> Option<FetchSource> {
for _ in 0..self.inner.len() {
let fetch_index = self.index;
self.index = (self.index + 1) % self.inner.len();
if let Some(source) = self.inner.get_index(fetch_index) {
if state_filter(source) {
return Some(source.clone());
}
}
}
None
}
pub(crate) fn add(&mut self, source: FetchSource) {
self.inner.insert(source);
}
pub(crate) fn retain(&mut self, filter: impl Fn(&FetchSource) -> bool) {
self.inner.retain(filter);
if !self.inner.is_empty() {
self.index %= self.inner.len();
}
}
}
#[derive(Debug, Default)]
pub(crate) struct SourceState {
current_state: SourceCurrentState,
}
impl SourceState {
pub fn should_use(&mut self) -> bool {
match &mut self.current_state {
SourceCurrentState::Available(_) => true,
SourceCurrentState::Backoff(backoff) => backoff.is_ready(),
}
}
pub fn is_valid(&mut self, config: FetchConfig) -> bool {
match &self.current_state {
SourceCurrentState::Available(num_timed_out) => {
if *num_timed_out > config.source_unavailable_timeout_threshold() {
self.current_state = SourceCurrentState::Backoff(FetchSourceBackoff::new(
FetchBackoff::new(config.source_retry_delay()),
NUM_PROBE_ATTEMPTS,
));
}
true
}
SourceCurrentState::Backoff(ref backoff) => !backoff.is_expired(),
}
}
pub fn record_timeout(&mut self) {
if let SourceCurrentState::Available(num_timeouts) = &mut self.current_state {
*num_timeouts += 1;
}
}
pub fn record_response(&mut self) {
match &self.current_state {
SourceCurrentState::Backoff(_) => {
self.current_state = SourceCurrentState::Available(0);
}
SourceCurrentState::Available(_) => (),
}
}
}
#[derive(Debug)]
pub enum SourceCurrentState {
Available(usize),
Backoff(FetchSourceBackoff),
}
impl default::Default for SourceCurrentState {
fn default() -> Self {
Self::Available(0)
}
}
#[derive(Debug)]
pub struct FetchSourceBackoff {
backoff: FetchBackoff,
probe_limit: u32,
probes: u32,
}
impl FetchSourceBackoff {
fn new(backoff: FetchBackoff, probe_limit: u32) -> Self {
Self {
backoff,
probe_limit,
probes: 0,
}
}
fn is_ready(&mut self) -> bool {
if self.backoff.is_ready() {
self.probes = self.probe_limit - 1; true
} else if self.probes > 0 {
self.probes -= 1;
true
} else {
false
}
}
fn is_expired(&self) -> bool {
self.backoff.is_expired()
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration};
use super::{SourceState, Sources, NUM_PROBE_ATTEMPTS};
use crate::{
backoff::{FetchBackoff, BACKOFF_RETRY_COUNT},
source::FetchSourceBackoff,
test_utils::*,
FetchPoolConfig,
};
#[test]
fn single_source() {
let mut sources = Sources::new([test_source(1)]);
assert_eq!(Some(test_source(1)), sources.next(|_| true));
assert_eq!(Some(test_source(1)), sources.next(|_| true));
assert_eq!(None, sources.next(|_| false));
}
#[test]
fn source_rotation() {
let mut sources = Sources::new([]);
sources.add(test_source(1));
sources.add(test_source(2));
assert_eq!(Some(test_source(1)), sources.next(|_| true));
assert_eq!(Some(test_source(2)), sources.next(|_| true));
assert_eq!(Some(test_source(1)), sources.next(|_| true));
sources.add(test_source(3));
assert_eq!(Some(test_source(2)), sources.next(|_| true));
assert_eq!(Some(test_source(3)), sources.next(|_| true));
assert_eq!(Some(test_source(1)), sources.next(|_| true));
}
#[tokio::test]
async fn fetch_source_backoff() {
let mut backoff = FetchSourceBackoff::new(FetchBackoff::new(Duration::from_millis(10)), 3);
assert!(!backoff.is_ready());
let mut num_tries = 0;
tokio::time::timeout(Duration::from_secs(2), async {
loop {
if backoff.is_ready() {
num_tries += 1;
} else if backoff.is_expired() {
break;
}
}
})
.await
.unwrap();
assert_eq!(3 * BACKOFF_RETRY_COUNT, num_tries);
}
#[test]
fn happy_path_source_state() {
let mut source_state: SourceState = Default::default();
let config = Arc::new(TestFetchConfig(1, 1));
for i in 0..500 {
assert!(source_state.should_use());
source_state.record_response();
if i % 100 == 0 {
assert!(source_state.is_valid(config.clone()));
}
}
assert!(source_state.should_use());
}
#[tokio::test(start_paused = true)]
async fn source_state_single_backoff_then_recover() {
let mut source_state: SourceState = Default::default();
let config = Arc::new(TestFetchConfig(1, 1));
assert!(source_state.should_use());
for _ in 0..=config.source_unavailable_timeout_threshold() {
assert!(source_state.should_use());
source_state.is_valid(config.clone());
source_state.record_timeout();
}
assert!(source_state.should_use());
source_state.is_valid(config.clone());
assert!(!source_state.should_use());
tokio::time::advance(Duration::from_secs(2)).await;
for _ in 0..NUM_PROBE_ATTEMPTS {
assert!(source_state.should_use());
}
assert!(!source_state.should_use());
source_state.record_response();
assert!(source_state.should_use());
}
#[tokio::test(start_paused = true)]
async fn source_state_backoff_to_expiry() {
let mut source_state: SourceState = Default::default();
let config = Arc::new(TestFetchConfig(1, 1));
assert!(source_state.should_use());
for _ in 0..=config.source_unavailable_timeout_threshold() {
source_state.record_timeout();
}
source_state.is_valid(config.clone());
assert!(!source_state.should_use());
for _ in 0..BACKOFF_RETRY_COUNT {
tokio::time::advance(100 * config.source_retry_delay()).await;
assert!(source_state.should_use());
}
assert!(!source_state.is_valid(config.clone()));
}
}