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},
};
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>,
minimum_segment_fetch_interval: Duration,
maximum_segment_fetch_interval: Duration,
idle_eviction_time: Duration,
min_segment_lifetime: Duration,
segment_lifetime_buffer: Duration,
}
impl SegmentManager {
pub fn new(
maximum_segment_fetch_interval: Duration,
minimum_segment_fetch_interval: Duration,
idle_eviction_time: Duration,
min_segment_lifetime: Duration,
segment_lifetime_buffer: Duration,
segment_fetcher: Box<dyn SegmentsDiscovery>,
) -> Self {
Self(Arc::new(SegmentManagerInner {
segments: scc::HashMap::new(),
segment_fetcher,
minimum_segment_fetch_interval,
maximum_segment_fetch_interval,
idle_eviction_time,
min_segment_lifetime,
segment_lifetime_buffer,
}))
}
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 })
}
pub async fn run(&self) {
loop {
let now = SystemTime::now();
let next = self.maintain(now).await;
let duration = next.duration_since(now).unwrap_or(Duration::ZERO);
tracing::trace!("Next segment manager maintenance in {:?}", duration);
tokio::time::sleep(duration).await;
}
}
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.minimum_segment_fetch_interval,
self.0.maximum_segment_fetch_interval,
self.0.min_segment_lifetime,
self.0.segment_lifetime_buffer,
);
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) => 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.maximum_segment_fetch_interval;
self.0
.segments
.retain_async(|key, entry| {
if !entry.has_references() && entry.last_use() + self.0.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.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.minimum_segment_fetch_interval,
self.0.maximum_segment_fetch_interval,
self.0.min_segment_lifetime,
self.0.segment_lifetime_buffer,
);
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,
min_refetch_delay: Duration,
max_refetch_delay: Duration,
min_segment_lifetime: Duration,
segment_lifetime_buffer: Duration,
) {
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 + max_refetch_delay;
for store in [&mut self.core_segments, &mut self.non_core_segments] {
store.retain(|_, segment| {
if segment.expiration < now + min_segment_lifetime {
return false;
}
let refetch_at = segment
.expiration
.checked_sub(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",
min_refetch_delay.as_secs()
);
next_refetch = now + min_refetch_delay;
}
next_refetch = next_refetch.max(now + min_refetch_delay);
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_wap2::test_util::{
Fixture, MAX_FETCH_INTERVAL, MIN_FETCH_INTERVAL, 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"
);
}
}