use std::{
collections::HashMap,
sync::{
Arc,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use anyhow::Context;
use endhost_api_models::SegmentsDiscovery;
use scc::hash_map::OccupiedEntry;
use sciparse::{
identifier::isd_asn::IsdAsn,
segment::{SegmentFp, Segments, SignedPathSegment},
};
use serde::{Deserialize, Serialize};
use serde_with::{DurationSeconds, serde_as};
use tokio::select;
const SEGMENT_FETCH_PAGE_SIZE: i32 = 250;
#[derive(Clone)]
pub struct SegmentManager(Arc<SegmentManagerInner>);
struct SegmentManagerInner {
segments: scc::HashMap<(IsdAsn, IsdAsn), SegmentStore>,
segment_fetcher: Box<dyn SegmentsDiscovery>,
config: SegmentManagerConfig,
next_maintain: tokio::sync::watch::Sender<SystemTime>,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SegmentManagerConfig {
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "SegmentManagerConfig::default_minimum_segment_fetch_interval")]
pub minimum_segment_fetch_interval: Duration,
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "SegmentManagerConfig::default_maximum_segment_fetch_interval")]
pub maximum_segment_fetch_interval: Duration,
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "SegmentManagerConfig::default_idle_eviction_time")]
pub idle_eviction_time: Duration,
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "SegmentManagerConfig::default_min_segment_lifetime")]
pub min_segment_lifetime: Duration,
#[serde_as(as = "DurationSeconds<u64>")]
#[serde(default = "SegmentManagerConfig::default_segment_lifetime_buffer")]
pub segment_lifetime_buffer: Duration,
}
impl SegmentManagerConfig {
fn default_minimum_segment_fetch_interval() -> Duration {
Duration::from_secs(30)
}
fn default_maximum_segment_fetch_interval() -> Duration {
Duration::from_secs(60 * 60)
}
fn default_idle_eviction_time() -> Duration {
Duration::from_secs(60)
}
fn default_min_segment_lifetime() -> Duration {
Duration::from_secs(60)
}
fn default_segment_lifetime_buffer() -> Duration {
Duration::from_secs(5 * 60)
}
pub fn validate(&self) -> anyhow::Result<()> {
if self.minimum_segment_fetch_interval > self.maximum_segment_fetch_interval {
anyhow::bail!(
"minimum_segment_fetch_interval ({:?}) is greater than \
maximum_segment_fetch_interval ({:?})",
self.minimum_segment_fetch_interval,
self.maximum_segment_fetch_interval
);
}
if self.minimum_segment_fetch_interval == Duration::ZERO {
anyhow::bail!("minimum_segment_fetch_interval must be greater than 0");
}
if self.maximum_segment_fetch_interval == Duration::ZERO {
anyhow::bail!("maximum_segment_fetch_interval must be greater than 0");
}
if self.idle_eviction_time == Duration::ZERO {
anyhow::bail!("idle_eviction_time must be greater than 0");
}
Ok(())
}
}
impl Default for SegmentManagerConfig {
fn default() -> Self {
Self {
minimum_segment_fetch_interval: Self::default_minimum_segment_fetch_interval(),
maximum_segment_fetch_interval: Self::default_maximum_segment_fetch_interval(),
idle_eviction_time: Self::default_idle_eviction_time(),
min_segment_lifetime: Self::default_min_segment_lifetime(),
segment_lifetime_buffer: Self::default_segment_lifetime_buffer(),
}
}
}
impl SegmentManager {
pub fn new(
config: SegmentManagerConfig,
segment_fetcher: Box<dyn SegmentsDiscovery>,
) -> anyhow::Result<Self> {
config.validate()?;
Ok(Self(Arc::new(SegmentManagerInner {
segments: scc::HashMap::new(),
segment_fetcher,
config,
next_maintain: tokio::sync::watch::channel(SystemTime::now()).0,
})))
}
pub async fn segment(&self, id: SegmentStoreId, now: SystemTime) -> Option<SignedPathSegment> {
let store = self.0.segments.get_async(&(id.src(), id.dst())).await?;
store.get().touch(now);
let segments = match id {
SegmentStoreId::Core { .. } => &store.get().core_segments,
SegmentStoreId::NonCore { .. } => &store.get().non_core_segments,
};
segments
.get(&id.fp())
.map(|segment_entry| segment_entry.segment.clone())
}
pub async fn hold_pair(
&self,
src: IsdAsn,
dst: IsdAsn,
now: SystemTime,
) -> anyhow::Result<PairGuard> {
let store = self.ensure_managed(src, dst, now).await?;
let ref_count = store.get().acquire();
tracing::trace!(%src, %dst, "Holding a pair against eviction");
Ok(PairGuard {
ref_count,
src,
dst,
})
}
pub async fn segments<'this>(
&'this self,
src: IsdAsn,
dst: IsdAsn,
now: SystemTime,
) -> anyhow::Result<SegmentsIter<'this>> {
let store = self.ensure_managed(src, dst, now).await?;
store.get().touch(now);
Ok(SegmentsIter { store })
}
async fn wait_to_next_maintain(&self) {
let mut rx = self.0.next_maintain.subscribe();
loop {
let next = *rx.borrow_and_update();
let duration = next
.duration_since(SystemTime::now())
.unwrap_or(Duration::ZERO);
tracing::trace!("Next segment manager maintenance in {:?}", duration);
select! {
_ = rx.changed() => {
tracing::trace!("Next segment manager maintenance time changed");
},
_ = tokio::time::sleep(duration) => return,
}
}
}
pub async fn run(&self) {
loop {
let now = SystemTime::now();
self.0
.next_maintain
.send_replace(now + self.0.config.maximum_segment_fetch_interval);
let next = self.maintain(now).await;
self.update_next_maintain(next);
self.wait_to_next_maintain().await;
}
}
fn update_next_maintain(&self, next: SystemTime) {
self.0.next_maintain.send_if_modified(|current| {
if next < *current {
*current = next;
true
} else {
false
}
});
}
async fn ensure_managed(
&self,
src: IsdAsn,
dst: IsdAsn,
now: SystemTime,
) -> anyhow::Result<OccupiedEntry<'_, (IsdAsn, IsdAsn), SegmentStore>> {
if let Some(entry) = self.0.segments.get_async(&(src, dst)).await {
entry.get().touch(now);
return Ok(entry);
}
tracing::info!(%src, %dst, "Fetching segments for a new (src, dst) pair");
let fetched = self
.0
.segment_fetcher
.list_segments(src, dst, SEGMENT_FETCH_PAGE_SIZE, String::new())
.await
.with_context(|| format!("Failed to fetch segments for ({src}, {dst})"))?;
let mut store = SegmentStore::new((src, dst), now);
store.update_segments(fetched.segments, now, &self.0.config);
match self.0.segments.entry_async((src, dst)).await {
scc::hash_map::Entry::Occupied(occupied_entry) => Ok(occupied_entry),
scc::hash_map::Entry::Vacant(vacant_entry) => {
self.update_next_maintain(store.next_refresh);
Ok(vacant_entry.insert_entry(store))
}
}
}
pub async fn maintain(&self, now: SystemTime) -> SystemTime {
let mut update_keys = Vec::new();
let mut next_maintain = now + self.0.config.maximum_segment_fetch_interval;
self.0
.segments
.retain_async(|key, entry| {
if !entry.has_references()
&& entry.last_use() + self.0.config.idle_eviction_time < now
{
tracing::debug!(
src = %key.0, dst = %key.1,
"Evicting unused segments from the store"
);
return false;
}
if entry.next_refresh < now {
update_keys.push(*key);
} else {
next_maintain = next_maintain.min(entry.next_refresh);
}
true
})
.await;
for (src, dst) in update_keys {
let fetched = self
.0
.segment_fetcher
.list_segments(src, dst, SEGMENT_FETCH_PAGE_SIZE, String::new())
.await;
let fetched = match fetched {
Ok(fetched) => fetched,
Err(e) => {
next_maintain =
next_maintain.min(now + self.0.config.minimum_segment_fetch_interval);
tracing::error!(%src, %dst, "Failed to fetch segments: {e}");
continue;
}
};
let Some(mut entry) = self.0.segments.get_async(&(src, dst)).await else {
tracing::debug!(
%src, %dst,
"Segments were evicted from the store before they could be updated"
);
continue;
};
entry
.get_mut()
.update_segments(fetched.segments, now, &self.0.config);
next_maintain = next_maintain.min(entry.get().next_refresh);
}
next_maintain
}
}
struct SegmentStore {
query: (IsdAsn, IsdAsn),
next_refresh: SystemTime,
last_use: AtomicU64,
refs: Arc<AtomicUsize>,
core_segments: HashMap<SegmentFp, SegmentEntry>,
non_core_segments: HashMap<SegmentFp, SegmentEntry>,
}
struct SegmentEntry {
segment: SignedPathSegment,
expiration: SystemTime,
}
impl SegmentStore {
fn new(query: (IsdAsn, IsdAsn), now: SystemTime) -> Self {
let this = Self {
query,
next_refresh: now,
last_use: AtomicU64::new(0),
refs: Arc::new(AtomicUsize::new(0)),
core_segments: HashMap::new(),
non_core_segments: HashMap::new(),
};
this.touch(now);
this
}
fn acquire(&self) -> Arc<AtomicUsize> {
self.refs.fetch_add(1, Ordering::Relaxed);
self.refs.clone()
}
fn has_references(&self) -> bool {
self.refs.load(Ordering::Relaxed) > 0
}
fn touch(&self, now: SystemTime) {
let secs = now
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
self.last_use.fetch_max(secs, Ordering::Relaxed);
}
fn last_use(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(self.last_use.load(Ordering::Relaxed))
}
fn update_segments(
&mut self,
new_segments: Segments,
now: SystemTime,
config: &SegmentManagerConfig,
) {
let start_core_count = self.core_segments.len();
let start_non_core_count = self.non_core_segments.len();
let (new_core, new_non_core) = new_segments.split_parts();
for (segments, store) in [
(new_core, &mut self.core_segments),
(new_non_core, &mut self.non_core_segments),
] {
for segment in segments {
let expiration = segment.expires_earliest();
store.insert(
segment.fingerprint(),
SegmentEntry {
segment,
expiration,
},
);
}
}
let mut next_refetch = now + config.maximum_segment_fetch_interval;
for store in [&mut self.core_segments, &mut self.non_core_segments] {
store.retain(|_, segment| {
if segment.expiration < now + config.min_segment_lifetime {
return false;
}
let refetch_at = segment
.expiration
.checked_sub(config.segment_lifetime_buffer)
.unwrap_or(now);
next_refetch = next_refetch.min(refetch_at);
true
});
}
if self.core_segments.is_empty() && self.non_core_segments.is_empty() {
tracing::debug!(
src = %self.query.0, dst = %self.query.1,
"Segment store is empty after update, forcing a refetch in {}s",
config.minimum_segment_fetch_interval.as_secs()
);
next_refetch = now + config.minimum_segment_fetch_interval;
}
next_refetch = next_refetch.max(now + config.minimum_segment_fetch_interval);
tracing::info!(
"Updated segments for ({}, {}): core: {} -> {}, non-core: {} -> {}, next refresh: {:?}",
self.query.0,
self.query.1,
start_core_count,
self.core_segments.len(),
start_non_core_count,
self.non_core_segments.len(),
next_refetch
);
self.next_refresh = next_refetch;
}
}
pub struct PairGuard {
ref_count: Arc<AtomicUsize>,
src: IsdAsn,
dst: IsdAsn,
}
impl Drop for PairGuard {
fn drop(&mut self) {
let previous = self.ref_count.fetch_sub(1, Ordering::Relaxed);
debug_assert!(
previous > 0,
"released a pair guard that was never acquired"
);
tracing::trace!(src = %self.src, dst = %self.dst, "Released a pair guard");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SegmentStoreId {
Core {
src: IsdAsn,
dst: IsdAsn,
fp: SegmentFp,
},
NonCore {
src: IsdAsn,
dst: IsdAsn,
fp: SegmentFp,
},
}
impl SegmentStoreId {
pub fn src(&self) -> IsdAsn {
match self {
Self::Core { src, .. } | Self::NonCore { src, .. } => *src,
}
}
pub fn dst(&self) -> IsdAsn {
match self {
Self::Core { dst, .. } | Self::NonCore { dst, .. } => *dst,
}
}
pub fn fp(&self) -> SegmentFp {
match self {
Self::Core { fp, .. } | Self::NonCore { fp, .. } => *fp,
}
}
pub fn is_core(&self) -> bool {
matches!(self, Self::Core { .. })
}
}
pub struct SegmentsIter<'store> {
store: OccupiedEntry<'store, (IsdAsn, IsdAsn), SegmentStore>,
}
impl SegmentsIter<'_> {
pub fn iter_core_segments(&self) -> impl Iterator<Item = &SignedPathSegment> {
self.store
.get()
.core_segments
.values()
.map(|entry| &entry.segment)
}
pub fn iter_non_core_segments(&self) -> impl Iterator<Item = &SignedPathSegment> {
self.store
.get()
.non_core_segments
.values()
.map(|entry| &entry.segment)
}
pub fn has_core_segment(&self, fp: SegmentFp) -> bool {
self.store.get().core_segments.contains_key(&fp)
}
pub fn has_non_core_segment(&self, fp: SegmentFp) -> bool {
self.store.get().non_core_segments.contains_key(&fp)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pg_wap::test_util::{
Fixture, IDLE_EVICTION_TIME, MAX_FETCH_INTERVAL, MIN_FETCH_INTERVAL, MIN_SEGMENT_LIFETIME,
MockFetcher, SEGMENT_LIFETIME_BUFFER, at, core_ia, leaf_ia, other_leaf_ia,
secs_since_epoch, short_lived_up_segment, store_id, up_segment,
};
#[tokio::test]
async fn segments_are_fetched_once_per_pair_and_queryable_by_fingerprint() {
let up = up_segment(0);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up.clone()]),
Duration::from_secs(100),
);
let fingerprints = {
let segments = fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are fetched");
segments
.iter_non_core_segments()
.map(SignedPathSegment::fingerprint)
.collect::<Vec<_>>()
};
assert_eq!(fingerprints, vec![up.fingerprint()]);
assert_eq!(fixture.fetcher.calls(), 1);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are served from the store");
assert_eq!(fixture.fetcher.calls(), 1);
let id = store_id(&up);
assert!(fixture.segments.segment(id, at(0)).await.is_some());
assert!(
fixture
.segments
.segment(
SegmentStoreId::NonCore {
src: leaf_ia(),
dst: core_ia(),
fp: SegmentFp::default(),
},
at(0)
)
.await
.is_none(),
"an unknown fingerprint is not found"
);
}
#[tokio::test]
async fn segments_close_to_expiry_are_not_stored() {
let up = up_segment(0);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up.clone()]),
Duration::from_secs(100),
);
let nearly_expired = at(secs_since_epoch(up.expires_earliest()) - 30);
let segments = fixture
.segments
.segments(leaf_ia(), core_ia(), nearly_expired)
.await
.expect("segments are fetched");
assert_eq!(segments.iter_non_core_segments().count(), 0);
}
#[tokio::test]
async fn maintenance_is_scheduled_no_later_than_the_fetch_interval_cap() {
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(100),
);
assert_eq!(
fixture.segments.maintain(at(0)).await,
at(0) + MAX_FETCH_INTERVAL,
"with nothing managed there is nothing to schedule around"
);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are fetched");
assert_eq!(
fixture.segments.maintain(at(0)).await,
at(0) + MAX_FETCH_INTERVAL,
"a long lived pair is still refetched at the cap"
);
assert_eq!(
fixture.fetcher.calls(),
1,
"nothing was due, so nothing was refetched"
);
}
#[tokio::test]
async fn maintenance_is_scheduled_a_buffer_before_the_earliest_expiry() {
let up = short_lived_up_segment(0, 4);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up.clone()]),
Duration::from_secs(100),
);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are fetched");
assert_eq!(
fixture.segments.maintain(at(0)).await,
up.expires_earliest() - SEGMENT_LIFETIME_BUFFER,
"the replacement has to be fetched before the segment expires"
);
}
#[tokio::test]
async fn maintenance_is_scheduled_for_the_earliest_of_all_pairs() {
let short = short_lived_up_segment(0, 4);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(100),
);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("the long lived pair is fetched");
fixture.fetcher.set_segments(Segments {
up_segments: vec![short.clone()],
..Segments::default()
});
fixture
.segments
.segments(leaf_ia(), other_leaf_ia(), at(0))
.await
.expect("the short lived pair is fetched");
assert_eq!(
fixture.segments.maintain(at(0)).await,
short.expires_earliest() - SEGMENT_LIFETIME_BUFFER,
"the pair that expires first decides when maintenance runs again"
);
}
#[tokio::test]
async fn a_pair_without_segments_is_retried_at_the_minimum_interval() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("the empty pair is managed");
assert_eq!(fixture.fetcher.calls(), 1);
assert_eq!(
fixture.segments.maintain(at(0)).await,
at(0) + MIN_FETCH_INTERVAL,
"a pair we know nothing about is retried as soon as we are allowed to"
);
fixture
.segments
.maintain(at(MIN_FETCH_INTERVAL.as_secs() + 1))
.await;
assert_eq!(fixture.fetcher.calls(), 2);
}
#[tokio::test]
async fn maintenance_reports_the_schedule_of_the_pairs_it_just_refetched() {
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(100),
);
let _guard = fixture
.segments
.hold_pair(leaf_ia(), core_ia(), at(0))
.await
.expect("the pair is fetched and held");
assert_eq!(fixture.fetcher.calls(), 1);
let due_at = MAX_FETCH_INTERVAL.as_secs() + 1;
assert_eq!(
fixture.segments.maintain(at(due_at)).await,
at(due_at) + MAX_FETCH_INTERVAL,
"the returned time must be the refetched pair's new schedule, not the one it was due at"
);
assert_eq!(fixture.fetcher.calls(), 2, "the pair was due and refetched");
}
#[tokio::test]
async fn a_failed_refetch_is_retried_at_the_minimum_interval() {
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(100),
);
let _guard = fixture
.segments
.hold_pair(leaf_ia(), core_ia(), at(0))
.await
.expect("the pair is fetched and held");
let due_at = MAX_FETCH_INTERVAL.as_secs() + 1;
fixture.fetcher.set_failing(true);
assert_eq!(
fixture.segments.maintain(at(due_at)).await,
at(due_at) + MIN_FETCH_INTERVAL,
"a failed fetch is retried as soon as we are allowed to"
);
assert_eq!(
fixture.fetcher.calls(),
2,
"the failing fetch was attempted"
);
fixture.fetcher.set_failing(false);
let retried_at = at(due_at + MIN_FETCH_INTERVAL.as_secs() + 1);
assert_eq!(
fixture.segments.maintain(retried_at).await,
retried_at + MAX_FETCH_INTERVAL
);
assert_eq!(fixture.fetcher.calls(), 3);
assert!(
fixture
.segments
.segment(store_id(&up_segment(0)), retried_at)
.await
.is_some(),
"the retry restored the segments of the pair"
);
}
#[tokio::test]
async fn unused_pairs_are_evicted_and_used_ones_kept() {
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up_segment(0)]),
Duration::from_secs(100),
);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are fetched");
assert_eq!(fixture.fetcher.calls(), 1);
fixture.segments.maintain(at(30)).await;
fixture
.segments
.segments(leaf_ia(), core_ia(), at(30))
.await
.expect("segments are still in the store");
assert_eq!(fixture.fetcher.calls(), 1, "the pair was still fresh");
fixture.segments.maintain(at(91)).await;
fixture
.segments
.segments(leaf_ia(), core_ia(), at(91))
.await
.expect("segments are fetched again");
assert_eq!(
fixture.fetcher.calls(),
2,
"the evicted pair has to be fetched again"
);
}
#[tokio::test]
async fn the_wake_up_only_moves_earlier() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
let next_maintain = &fixture.segments.0.next_maintain;
next_maintain.send_replace(at(1000));
fixture.segments.update_next_maintain(at(500));
assert_eq!(*next_maintain.borrow(), at(500), "an earlier wake up wins");
fixture.segments.update_next_maintain(at(900));
assert_eq!(
*next_maintain.borrow(),
at(500),
"a later wake up does not push the earlier one back"
);
}
#[tokio::test]
async fn a_new_pair_moves_the_wake_up_to_its_own_refresh() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
let next_maintain = &fixture.segments.0.next_maintain;
next_maintain.send_replace(at(0) + MAX_FETCH_INTERVAL);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are fetched");
assert_eq!(
*next_maintain.borrow(),
at(0) + MIN_FETCH_INTERVAL,
"a pair added after the last maintenance run has to be waited for"
);
next_maintain.send_replace(at(0) + MAX_FETCH_INTERVAL);
fixture
.segments
.segments(leaf_ia(), core_ia(), at(0))
.await
.expect("segments are served from the store");
assert_eq!(
*next_maintain.borrow(),
at(0) + MAX_FETCH_INTERVAL,
"a pair that is already managed is already scheduled for"
);
}
#[tokio::test(start_paused = true)]
async fn the_wait_ends_at_the_scheduled_wake_up() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture
.segments
.0
.next_maintain
.send_replace(SystemTime::now() + Duration::from_secs(30));
let started = tokio::time::Instant::now();
fixture.segments.wait_to_next_maintain().await;
let waited = started.elapsed();
assert!(
waited > Duration::from_secs(29) && waited <= Duration::from_secs(31),
"waited {waited:?} instead of the scheduled 30s"
);
}
#[tokio::test(start_paused = true)]
async fn an_earlier_wake_up_cuts_the_wait_short() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture
.segments
.0
.next_maintain
.send_replace(SystemTime::now() + MAX_FETCH_INTERVAL);
let started = tokio::time::Instant::now();
let segments = fixture.segments.clone();
let waiting = tokio::spawn(async move { segments.wait_to_next_maintain().await });
tokio::time::sleep(Duration::from_secs(10)).await;
fixture
.segments
.update_next_maintain(SystemTime::now() + Duration::from_secs(5));
tokio::time::timeout(MAX_FETCH_INTERVAL / 2, waiting)
.await
.expect("the wait ends before the wake up it started with")
.expect("the waiting task does not panic");
let waited = started.elapsed();
assert!(
waited < Duration::from_secs(20),
"waited {waited:?} instead of following the earlier wake up"
);
}
#[tokio::test]
async fn the_maintenance_loop_wakes_for_a_pair_added_while_it_waits() {
let fetcher = MockFetcher::empty();
let segments = SegmentManager::new(
SegmentManagerConfig {
minimum_segment_fetch_interval: Duration::from_millis(50),
maximum_segment_fetch_interval: Duration::from_secs(60),
idle_eviction_time: IDLE_EVICTION_TIME,
min_segment_lifetime: MIN_SEGMENT_LIFETIME,
segment_lifetime_buffer: SEGMENT_LIFETIME_BUFFER,
},
Box::new(fetcher.clone()),
)
.expect("a valid SegmentManagerConfig");
let maintained = segments.clone();
tokio::spawn(async move { maintained.run().await });
tokio::time::sleep(Duration::from_millis(20)).await;
assert_eq!(fetcher.calls(), 0, "nothing asked for a pair yet");
segments
.segments(leaf_ia(), core_ia(), SystemTime::now())
.await
.expect("segments are fetched");
assert_eq!(fetcher.calls(), 1);
let mut calls = fetcher.calls();
for _ in 0..50 {
if calls > 1 {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
calls = fetcher.calls();
}
assert!(
calls > 1,
"the loop kept waiting instead of maintaining the pair added while it waited"
);
}
}