use indexmap::map::Entry;
use kitsune_p2p_timestamp::Timestamp;
use std::{collections::HashMap, sync::Arc};
use tokio::time::{Duration, Instant};
use kitsune_p2p_types::{tx_utils::ShareOpen, KSpace};
use crate::{
queue::MapQueue,
source::{FetchSource, SourceState, Sources},
FetchContext, FetchKey, FetchPoolPush, RoughInt, TransferMethod,
};
mod pool_reader;
pub use pool_reader::*;
#[derive(Clone)]
pub struct FetchPool {
config: FetchConfig,
state: ShareOpen<State>,
}
impl std::fmt::Debug for FetchPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.state
.share_ref(|state| f.debug_struct("FetchPool").field("state", state).finish())
}
}
pub type FetchConfig = Arc<dyn FetchPoolConfig>;
pub trait FetchPoolConfig: 'static + Send + Sync {
fn item_retry_delay(&self) -> Duration {
Duration::from_secs(90)
}
fn source_retry_delay(&self) -> Duration {
Duration::from_secs(30)
}
fn merge_fetch_contexts(&self, a: u32, b: u32) -> u32;
fn fetch_batch_size(&self) -> usize {
100
}
fn source_unavailable_timeout_threshold(&self) -> usize {
30
}
}
struct FetchPoolConfigBitwiseOr;
impl FetchPoolConfig for FetchPoolConfigBitwiseOr {
fn merge_fetch_contexts(&self, a: u32, b: u32) -> u32 {
a | b
}
}
#[derive(Debug, Default)]
pub(crate) struct State {
queue: MapQueue<FetchKey, FetchPoolItem>,
sources: HashMap<FetchSource, SourceState>,
}
impl FetchPool {
pub fn new(config: FetchConfig) -> Self {
Self {
config,
state: ShareOpen::new(State::default()),
}
}
pub fn new_bitwise_or() -> Self {
Self {
config: Arc::new(FetchPoolConfigBitwiseOr),
state: ShareOpen::new(State::default()),
}
}
pub fn push(&self, args: FetchPoolPush) {
self.state.share_mut(|s| {
tracing::debug!(
"FetchPool (size = {}) item added: {:?}",
s.queue.len() + 1,
args
);
s.push(&*self.config, args);
});
}
pub fn check_item(&self, key: &FetchKey) -> (bool, Option<FetchContext>) {
self.state.share_ref(|s| match s.queue.get(key) {
Some(item) => (true, item.context),
None => (false, None),
})
}
pub fn remove(&self, key: &FetchKey) -> Option<FetchPoolItem> {
self.state.share_mut(|s| {
let removed = s.remove(key);
tracing::debug!(
"FetchPool (size = {}) item removed: key={:?} val={:?}",
s.queue.len(),
key,
removed
);
removed
})
}
pub fn get_items_to_fetch(&self) -> Vec<(FetchKey, KSpace, FetchSource, Option<FetchContext>)> {
self.state
.share_mut(|s| s.get_batch(self.config.clone()).clone())
}
pub fn len(&self) -> usize {
self.state.share_ref(|s| s.queue.len())
}
pub fn is_empty(&self) -> bool {
self.state.share_ref(|s| s.queue.is_empty())
}
pub fn check_sources(&self) {
self.state.share_mut(|s| {
s.check_sources(self.config.clone());
});
}
}
impl State {
pub fn push(&mut self, config: &dyn FetchPoolConfig, args: FetchPoolPush) {
let FetchPoolPush {
key,
context,
space,
source,
size,
transfer_method,
} = args;
self.sources.entry(source.clone()).or_default();
match self.queue.entry(key) {
Entry::Vacant(e) => {
let sources = Sources::new([source.clone()]);
let item = FetchPoolItem {
sources,
space,
size,
first_transfer_info: (transfer_method, Timestamp::now()),
context,
pending_response: None,
};
e.insert(item);
}
Entry::Occupied(mut e) => {
let v = e.get_mut();
v.sources.add(source.clone());
v.context = match (v.context.take(), context) {
(Some(a), Some(b)) => Some(config.merge_fetch_contexts(*a, *b).into()),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
}
}
pub fn get_batch(
&mut self,
config: Arc<dyn FetchPoolConfig>,
) -> Vec<(FetchKey, KSpace, FetchSource, Option<FetchContext>)> {
let batch_size = config.fetch_batch_size();
let mut to_fetch = vec![];
for _ in 0..self.queue.len() {
if to_fetch.len() >= batch_size {
break;
}
let (key, item) = match self.queue.front() {
Some(item) => item,
None => continue,
};
let should_fetch_item = match &item.pending_response {
Some(pending_response) => {
if pending_response.when.elapsed() > config.item_retry_delay() {
if let Some(state) = self.sources.get_mut(&pending_response.source) {
state.record_timeout();
}
true
} else {
false
}
}
None => true,
};
if should_fetch_item {
item.pending_response = None;
if let Some(source) = item.sources.next(|source| {
match self.sources.get_mut(source) {
Some(state) => {
if state.should_use() {
return true;
}
}
_ => {
tracing::warn!(
"Not considering source because it is not registered: {:?}",
source
);
}
}
false
}) {
let space = item.space.clone();
item.pending_response = Some(PendingItemResponse {
when: Instant::now(),
source: source.clone(),
});
to_fetch.push((key.clone(), space, source, item.context));
}
}
}
to_fetch
}
pub fn remove(&mut self, key: &FetchKey) -> Option<FetchPoolItem> {
match self.queue.remove(key) {
Some(item) => {
if let Some(pending) = item.pending_response.as_ref() {
if let Some(state) = self.sources.get_mut(&pending.source) {
state.record_response();
}
}
Some(item)
}
None => None,
}
}
pub fn check_sources(&mut self, config: FetchConfig) {
self.sources
.retain(|_, source| source.is_valid(config.clone()));
let keys: Vec<_> = self.queue.keys().cloned().collect();
for key in keys {
self.queue
.get_mut(&key)
.expect("Iterating keys")
.sources
.retain(|s| self.sources.contains_key(s));
if self
.queue
.get(&key)
.expect("Iterating keys")
.sources
.is_empty()
{
self.queue.remove(&key);
}
}
}
#[cfg(any(test, feature = "test_utils"))]
#[allow(dead_code)]
pub fn summary(&self) -> String {
use human_repr::HumanCount;
let table = self
.queue
.iter()
.map(|(k, v)| {
let key = match k {
FetchKey::Op(hash) => {
let h = hash.to_string();
format!("{}..{}", &h[0..4], &h[h.len() - 4..])
}
};
let size = v.size.unwrap_or_default().get();
format!(
"{:10} {:^6} {:^6} {:>6}",
key,
v.sources.len(),
v.pending_response
.as_ref()
.map(|t| format!("{:?}", t.when.elapsed()))
.unwrap_or_else(|| "-".to_string()),
size.human_count_bytes(),
)
})
.collect::<Vec<_>>()
.join("\n");
format!("{}\n{} items total", table, self.queue.len())
}
#[cfg(any(test, feature = "test_utils"))]
#[allow(dead_code)]
pub fn summary_heading() -> String {
format!("{:10} {:>6} {:>6} {}", "key", "#src", "last", "size")
}
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(Clone))]
pub struct FetchPoolItem {
sources: Sources,
space: KSpace,
size: Option<RoughInt>,
first_transfer_info: (TransferMethod, Timestamp),
pub context: Option<FetchContext>,
pending_response: Option<PendingItemResponse>,
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(Clone))]
pub struct PendingItemResponse {
when: Instant,
source: FetchSource,
}
#[cfg(test)]
mod tests {
use crate::backoff::BACKOFF_RETRY_COUNT;
use crate::test_utils::*;
use crate::TransferMethod;
use arbitrary::Arbitrary;
use arbitrary::Unstructured;
use kitsune_p2p_types::fetch_pool::GossipType;
use pretty_assertions::assert_eq;
use rand::RngCore;
use std::collections::HashSet;
use std::{sync::Arc, time::Duration};
use kitsune_p2p_types::bin_types::{KitsuneBinType, KitsuneSpace};
use super::*;
pub(super) fn item(
_cfg: Arc<dyn FetchPoolConfig>,
sources: Vec<FetchSource>,
context: Option<FetchContext>,
) -> FetchPoolItem {
FetchPoolItem {
sources: Sources::new(sources),
space: Arc::new(KitsuneSpace::new(vec![0; 36])),
context,
first_transfer_info: (TransferMethod::Publish, Timestamp::now()),
size: None,
pending_response: None,
}
}
fn arbitrary_test_sources(u: &mut Unstructured, count: usize) -> Vec<FetchSource> {
test_sources(std::iter::repeat_with(|| u8::arbitrary(u).unwrap()).take(count))
}
#[test]
fn state_keeps_context_on_merge_if_new_is_none() {
let mut q = State::default();
let cfg = TestFetchConfig(1, 1);
q.push(&cfg, test_req_op(1, test_ctx(1), test_source(1)));
assert_eq!(test_ctx(1), q.queue.front().unwrap().1.context);
q.push(&cfg, test_req_op(1, None, test_source(0)));
assert_eq!(test_ctx(1), q.queue.front().unwrap().1.context);
}
#[test]
fn state_adds_context_on_merge_if_current_is_none() {
let mut q = State::default();
let cfg = TestFetchConfig(1, 1);
q.push(&cfg, test_req_op(1, None, test_source(1)));
assert_eq!(None, q.queue.front().unwrap().1.context);
q.push(&cfg, test_req_op(1, test_ctx(1), test_source(0)));
assert_eq!(test_ctx(1), q.queue.front().unwrap().1.context);
}
#[test]
fn state_can_merge_two_items_without_contexts() {
let mut q = State::default();
let cfg = TestFetchConfig(1, 1);
q.push(&cfg, test_req_op(1, None, test_source(1)));
assert_eq!(None, q.queue.front().unwrap().1.context);
q.push(&cfg, test_req_op(1, None, test_source(0)));
assert_eq!(None, q.queue.front().unwrap().1.context);
assert_eq!(2, q.queue.front().unwrap().1.sources.len());
}
#[test]
fn state_ignores_duplicate_sources_on_merge() {
let mut q = State::default();
let cfg = TestFetchConfig(1, 1);
q.push(&cfg, test_req_op(1, test_ctx(1), test_source(1)));
assert_eq!(1, q.queue.front().unwrap().1.sources.len());
q.push(&cfg, test_req_op(1, test_ctx(2), test_source(1)));
assert_eq!(1, q.queue.front().unwrap().1.sources.len());
}
#[test]
fn queue_push() {
let mut q = State::default();
let cfg = Arc::new(TestFetchConfig(1, 1));
let ts = Timestamp::now();
q.push(&*cfg, test_req_op(1, test_ctx(0), test_source(0)));
q.push(&*cfg, test_req_op(1, test_ctx(1), test_source(1)));
q.push(&*cfg, test_req_op(2, test_ctx(0), test_source(0)));
fn update_timestamp(
ts: Timestamp,
q: &MapQueue<FetchKey, FetchPoolItem>,
) -> Vec<(FetchKey, FetchPoolItem)> {
(**q)
.clone()
.into_iter()
.map(|(key, mut item)| {
item.first_transfer_info.1 = ts;
(key, item)
})
.collect()
}
let expected_ready: MapQueue<FetchKey, FetchPoolItem> = [
(
test_key_op(1),
item(cfg.clone(), test_sources(0..=1), test_ctx(1)),
),
(test_key_op(2), item(cfg, test_sources([0]), test_ctx(0))),
]
.into_iter()
.map(|(key, mut item)| {
item.first_transfer_info.1 = ts;
(key, item)
})
.collect();
assert_eq!(
update_timestamp(ts, &q.queue),
update_timestamp(ts, &expected_ready)
)
}
#[tokio::test(start_paused = true)]
async fn queue_next() {
let cfg = Arc::new(TestFetchConfig(5, 10));
let mut q = {
let mut queue = [
(
test_key_op(1),
item(cfg.clone(), test_sources(0..=2), test_ctx(1)),
),
(
test_key_op(2),
item(cfg.clone(), test_sources(1..=3), test_ctx(1)),
),
(
test_key_op(3),
item(cfg.clone(), test_sources(2..=4), test_ctx(1)),
),
];
queue[1].1.pending_response = Some(PendingItemResponse {
when: Instant::now() - Duration::from_secs(3),
source: test_source(1),
});
let queue = queue.into_iter().collect();
State {
queue,
sources: test_sources(0..=4)
.into_iter()
.map(|s| (s, SourceState::default()))
.collect(),
}
};
assert_eq!(2, q.get_batch(cfg.clone()).len());
tokio::time::advance(Duration::from_secs(3)).await;
assert_eq!(1, q.get_batch(cfg.clone()).len());
tokio::time::advance(Duration::from_secs(10)).await;
assert_eq!(3, q.get_batch(cfg.clone()).len());
}
#[tokio::test(start_paused = true)]
async fn uses_all_sources() {
let cfg = Arc::new(TestFetchConfig(1, 10));
let num_items = 10;
let mut q = {
let mut queue = vec![];
let mut sources = vec![];
for i in 0..num_items {
let these_sources =
test_sources((i * num_items) as u8..(i * num_items + num_items) as u8);
queue.push((
test_key_op(i as u8),
item(cfg.clone(), these_sources.clone(), test_ctx(1)),
));
sources.extend(these_sources);
}
State {
queue: queue.into_iter().collect(),
sources: sources
.into_iter()
.map(|s| (s, SourceState::default()))
.collect(),
}
};
let mut seen_sources = HashSet::new();
for _ in 0..num_items {
q.get_batch(cfg.clone())
.into_iter()
.map(|item| match item.2 {
FetchSource::Agent(a) => a.0.clone(),
})
.for_each(|source| {
seen_sources.insert(source);
});
tokio::time::advance(Duration::from_secs(30)).await;
}
assert_eq!(num_items * num_items, seen_sources.len());
}
#[tokio::test(start_paused = true)]
async fn remove_fetch_item() {
holochain_trace::test_run();
let cfg = Arc::new(TestFetchConfig(1, 10));
let mut q: State = {
let queue = [(
test_key_op(1),
item(cfg.clone(), test_sources([1]), test_ctx(1)),
)];
let queue = queue.into_iter().collect();
let sources = [(test_source(1), SourceState::default())]
.into_iter()
.collect();
State { queue, sources }
};
assert_eq!(1, q.get_batch(cfg.clone()).len());
q.remove(&test_key_op(1));
tokio::time::advance(Duration::from_secs(30)).await;
assert_eq!(0, q.get_batch(cfg).len());
}
#[tokio::test(start_paused = true)]
async fn fetch_pool() {
struct TestFetchConfig {}
impl FetchPoolConfig for TestFetchConfig {
fn merge_fetch_contexts(&self, a: u32, b: u32) -> u32 {
a | b
}
}
let mut noise = [0; 1_000];
rand::thread_rng().fill_bytes(&mut noise);
let mut u = Unstructured::new(&noise);
let fetch_pool = FetchPool::new(Arc::new(TestFetchConfig {}));
let unavailable_sources: HashSet<FetchSource> =
arbitrary_test_sources(&mut u, 10).into_iter().collect();
fetch_pool.push(FetchPoolPush {
key: test_key_op(220),
space: test_space(u8::arbitrary(&mut u).unwrap()),
source: unavailable_sources.iter().last().cloned().unwrap(),
size: None, context: test_ctx(u32::arbitrary(&mut u).unwrap()),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
let mut failed_count = 0;
for i in (0..200).step_by(5) {
for _ in 0..5 {
fetch_pool.push(FetchPoolPush {
key: test_key_op(i),
space: test_space(u8::arbitrary(&mut u).unwrap()),
source: test_source(u8::arbitrary(&mut u).unwrap()),
size: None, context: test_ctx(u32::arbitrary(&mut u).unwrap()),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
}
let items = fetch_pool.get_items_to_fetch();
for item in items {
if !unavailable_sources.contains(&item.2) {
fetch_pool.remove(&item.0);
} else {
failed_count += 1;
}
}
tokio::time::advance(fetch_pool.config.item_retry_delay()).await;
}
assert!(
!fetch_pool.get_items_to_fetch().is_empty(),
"Pool should have had at least one item but got \n {}",
fetch_pool.state.share_ref(|s| format!(
"{}\n{}",
State::summary_heading(),
s.summary()
))
);
assert!(
failed_count >= 10,
"At least 10 items should have failed to be fetched but was {}",
failed_count
);
}
#[test]
fn check_item_missing() {
let fetch_pool = FetchPool::new(Arc::new(TestFetchConfig(1, 1)));
assert_eq!((false, None), fetch_pool.check_item(&test_key_op(1)));
}
#[test]
fn drain_fetch_pool() {
struct TestFetchConfig {}
impl FetchPoolConfig for TestFetchConfig {
fn merge_fetch_contexts(&self, a: u32, b: u32) -> u32 {
a | b
}
}
let fetch_pool = FetchPool::new(Arc::new(TestFetchConfig {}));
for i in (0..200).step_by(5) {
for j in 0..5 {
fetch_pool.push(FetchPoolPush {
key: test_key_op(i),
space: test_space(j),
source: test_source(j),
size: None, context: test_ctx(0),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
}
}
for _ in 0..2 {
for (key, _, _, _) in fetch_pool.get_items_to_fetch() {
if fetch_pool.check_item(&key).0 {
fetch_pool.remove(&key);
}
}
}
assert!(fetch_pool.is_empty());
assert_eq!(0, fetch_pool.get_items_to_fetch().len());
}
#[tokio::test(start_paused = true)]
async fn drop_expired_sources() {
let config = Arc::new(TestFetchConfig(1, 1));
let fetch_pool = FetchPool::new(config.clone());
fetch_pool.push(FetchPoolPush {
key: test_key_op(1),
space: test_space(1),
source: test_source(1),
size: None,
context: test_ctx(0),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
fetch_pool.push(FetchPoolPush {
key: test_key_op(2),
space: test_space(1),
source: test_source(1),
size: None,
context: test_ctx(0),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
fetch_pool.push(FetchPoolPush {
key: test_key_op(2),
space: test_space(1),
source: test_source(2),
size: None,
context: test_ctx(0),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
for _ in 0..(config.source_unavailable_timeout_threshold() + 1) {
fetch_pool.get_items_to_fetch();
tokio::time::advance(2 * config.item_retry_delay()).await;
}
fetch_pool.check_sources();
for _ in 0..BACKOFF_RETRY_COUNT {
tokio::time::advance(1000 * config.source_retry_delay()).await;
assert_eq!(2, fetch_pool.get_items_to_fetch().len());
}
let keep_source_two_alive_key = test_key_op(5);
fetch_pool.push(FetchPoolPush {
key: keep_source_two_alive_key.clone(),
space: test_space(1),
source: test_source(2),
size: None,
context: test_ctx(0),
transfer_method: TransferMethod::Gossip(GossipType::Recent),
});
assert!(fetch_pool.check_item(&keep_source_two_alive_key).0);
fetch_pool.remove(&keep_source_two_alive_key);
fetch_pool.check_sources();
assert_eq!(1, fetch_pool.len());
tokio::time::advance(2 * config.item_retry_delay()).await;
let batch = fetch_pool.get_items_to_fetch();
assert_eq!(1, batch.len());
assert_eq!(test_source(2), batch.first().unwrap().2);
}
#[test]
fn default_fetch_context_merge_maintains_flags_from_both_contexts() {
const FLAG_1: u32 = 1 << 5;
const FLAG_2: u32 = 1 << 10;
let context_1 = FetchContext(FLAG_1);
let context_2 = FetchContext(FLAG_2);
let pool = FetchPool::new_bitwise_or();
let merged = pool.config.merge_fetch_contexts(*context_1, *context_2);
assert_eq!(FLAG_1, merged & FLAG_1);
assert_eq!(FLAG_2, merged & FLAG_2);
assert_eq!(0, merged ^ (FLAG_1 | FLAG_2)); }
}