use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use aion_store::{NamespacePlacement, NamespaceStore};
#[derive(Clone)]
struct CachedPlacement {
placement: NamespacePlacement,
fetched_at: Instant,
}
#[derive(Clone)]
pub struct PlacementCache {
store: Arc<dyn NamespaceStore>,
ttl: Duration,
entries: Arc<Mutex<HashMap<String, CachedPlacement>>>,
}
impl std::fmt::Debug for PlacementCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PlacementCache")
.field("ttl", &self.ttl)
.finish_non_exhaustive()
}
}
impl PlacementCache {
#[must_use]
pub fn new(store: Arc<dyn NamespaceStore>, ttl: Duration) -> Self {
Self {
store,
ttl,
entries: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn placement(&self, namespace: &str) -> NamespacePlacement {
if let Some(hit) = self.fresh_hit(namespace) {
return hit;
}
let placement = match self.store.get_namespace(namespace).await {
Ok(Some(record)) => record.placement,
Ok(None) | Err(_) => NamespacePlacement::Unplaced,
};
self.store_entry(namespace, &placement);
placement
}
fn fresh_hit(&self, namespace: &str) -> Option<NamespacePlacement> {
let entries = self.entries.lock().ok()?;
let entry = entries.get(namespace)?;
if entry.fetched_at.elapsed() < self.ttl {
Some(entry.placement.clone())
} else {
None
}
}
fn store_entry(&self, namespace: &str, placement: &NamespacePlacement) {
if let Ok(mut entries) = self.entries.lock() {
entries.insert(
namespace.to_owned(),
CachedPlacement {
placement: placement.clone(),
fetched_at: Instant::now(),
},
);
}
}
}
#[must_use]
pub fn preferred_node_order(placement: &NamespacePlacement) -> Vec<Option<String>> {
match placement {
NamespacePlacement::Prefer { nodes } => {
let mut tiers: Vec<Option<String>> =
nodes.iter().map(|label| Some(label.clone())).collect();
tiers.push(None);
tiers
}
NamespacePlacement::Unplaced | NamespacePlacement::Pinned { .. } => vec![None],
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkerSelection {
PreferTiers(Vec<Option<String>>),
Required(BTreeSet<String>),
}
#[must_use]
pub fn worker_selection_for(placement: &NamespacePlacement) -> WorkerSelection {
match placement {
NamespacePlacement::Pinned { nodes } => WorkerSelection::Required(nodes.clone()),
other => WorkerSelection::PreferTiers(preferred_node_order(other)),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use aion_store::{InMemoryStore, NamespaceOrigin, NamespacePlacement, NamespaceStore};
use super::PlacementCache;
fn labels(values: &[&str]) -> BTreeSet<String> {
values.iter().map(|v| (*v).to_owned()).collect()
}
#[tokio::test]
async fn reads_placement_from_store_and_serves_a_fresh_hit()
-> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
store
.register_namespace("orders", NamespaceOrigin::Explicit)
.await?;
store
.set_namespace_placement(
"orders",
NamespacePlacement::Prefer {
nodes: labels(&["n1"]),
},
)
.await?;
let cache = PlacementCache::new(Arc::clone(&store), Duration::from_secs(60));
let first = cache.placement("orders").await;
assert_eq!(
first,
NamespacePlacement::Prefer {
nodes: labels(&["n1"])
}
);
store
.set_namespace_placement("orders", NamespacePlacement::Unplaced)
.await?;
let cached = cache.placement("orders").await;
assert_eq!(
cached,
NamespacePlacement::Prefer {
nodes: labels(&["n1"])
},
"a fresh cache hit must not re-read the mutated durable record"
);
Ok(())
}
#[tokio::test]
async fn refreshes_after_ttl_expiry() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
store
.register_namespace("orders", NamespaceOrigin::Explicit)
.await?;
store
.set_namespace_placement(
"orders",
NamespacePlacement::Prefer {
nodes: labels(&["n1"]),
},
)
.await?;
let cache = PlacementCache::new(Arc::clone(&store), Duration::ZERO);
assert_eq!(
cache.placement("orders").await,
NamespacePlacement::Prefer {
nodes: labels(&["n1"])
}
);
store
.set_namespace_placement("orders", NamespacePlacement::Unplaced)
.await?;
assert_eq!(
cache.placement("orders").await,
NamespacePlacement::Unplaced,
"an expired entry must re-read the mutated durable record"
);
Ok(())
}
#[tokio::test]
async fn absent_namespace_defaults_to_unplaced() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
let cache = PlacementCache::new(store, Duration::from_secs(60));
assert_eq!(
cache.placement("never-seen").await,
NamespacePlacement::Unplaced,
"an absent registry row defaults to Unplaced (any worker)"
);
Ok(())
}
use super::preferred_node_order;
#[test]
fn prefer_order_is_each_label_then_the_none_spill() {
let order = preferred_node_order(&NamespacePlacement::Prefer {
nodes: labels(&["n2", "n1"]),
});
assert_eq!(
order,
vec![Some("n1".to_owned()), Some("n2".to_owned()), None],
"each preferred label (sorted) precedes the None spill tier"
);
}
#[test]
fn empty_prefer_set_is_just_the_spill() {
let order = preferred_node_order(&NamespacePlacement::Prefer {
nodes: BTreeSet::new(),
});
assert_eq!(order, vec![None], "an empty prefer set is the spill case");
}
#[test]
fn unplaced_is_a_single_any_worker_tier() {
assert_eq!(
preferred_node_order(&NamespacePlacement::Unplaced),
vec![None]
);
}
use super::{WorkerSelection, worker_selection_for};
#[test]
fn prefer_selects_ordered_tiers_with_a_none_spill() {
assert_eq!(
worker_selection_for(&NamespacePlacement::Prefer {
nodes: labels(&["n2", "n1"]),
}),
WorkerSelection::PreferTiers(vec![Some("n1".to_owned()), Some("n2".to_owned()), None,]),
);
}
#[test]
fn unplaced_selects_the_single_any_worker_tier() {
assert_eq!(
worker_selection_for(&NamespacePlacement::Unplaced),
WorkerSelection::PreferTiers(vec![None]),
);
}
#[test]
fn pinned_selects_required_labels_and_never_spills() {
let selection = worker_selection_for(&NamespacePlacement::Pinned {
nodes: labels(&["n1", "n2"]),
});
assert_eq!(
selection,
WorkerSelection::Required(labels(&["n1", "n2"])),
"Pinned must require its label set with NO None spill tier"
);
}
}