use std::collections::{BTreeMap, VecDeque};
use sim_kernel::{Error, Expr, Result, Symbol};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StoreKey(Symbol);
impl StoreKey {
pub fn new(symbol: Symbol) -> Self {
Self(symbol)
}
pub fn named(name: impl Into<String>) -> Self {
Self(Symbol::qualified("stream/content", name.into()))
}
pub fn as_symbol(&self) -> &Symbol {
&self.0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContentFrame {
key: StoreKey,
session: Symbol,
receipt_seq: u64,
inserted_tick: u64,
size_bytes: usize,
value: Expr,
}
impl ContentFrame {
pub fn new(
key: StoreKey,
session: Symbol,
receipt_seq: u64,
inserted_tick: u64,
size_bytes: usize,
value: Expr,
) -> Self {
Self {
key,
session,
receipt_seq,
inserted_tick,
size_bytes,
value,
}
}
pub fn key(&self) -> &StoreKey {
&self.key
}
pub fn session(&self) -> &Symbol {
&self.session
}
pub fn receipt_seq(&self) -> u64 {
self.receipt_seq
}
pub fn inserted_tick(&self) -> u64 {
self.inserted_tick
}
pub fn size_bytes(&self) -> usize {
self.size_bytes
}
pub fn value(&self) -> &Expr {
&self.value
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RetentionWindow {
pub session: Symbol,
pub receipt_seq: u64,
pub retain_ms: u64,
}
impl RetentionWindow {
pub fn new(session: Symbol, receipt_seq: u64, retain_ms: u64) -> Self {
Self {
session,
receipt_seq,
retain_ms,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoreEvicted {
pub key: StoreKey,
pub reason: Symbol,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundedContentStore {
max_bytes: usize,
used_bytes: usize,
entries: BTreeMap<StoreKey, ContentFrame>,
order: VecDeque<StoreKey>,
}
impl BoundedContentStore {
pub fn new(max_bytes: usize) -> Result<Self> {
if max_bytes == 0 {
return Err(Error::Eval(
"content store bound must be greater than zero".to_owned(),
));
}
Ok(Self {
max_bytes,
used_bytes: 0,
entries: BTreeMap::new(),
order: VecDeque::new(),
})
}
pub fn insert(&mut self, frame: ContentFrame) -> Result<Vec<StoreEvicted>> {
if frame.size_bytes > self.max_bytes {
return Err(Error::Eval(format!(
"content frame {} bytes exceeds store bound {}",
frame.size_bytes, self.max_bytes
)));
}
let mut evicted = Vec::new();
if self.remove_existing(&frame.key).is_some() {
evicted.push(StoreEvicted {
key: frame.key.clone(),
reason: replaced_reason(),
});
}
while self.used_bytes.saturating_add(frame.size_bytes) > self.max_bytes {
if let Some(item) = self.evict_oldest(size_bound_reason()) {
evicted.push(item);
} else {
break;
}
}
self.used_bytes = self.used_bytes.saturating_add(frame.size_bytes);
self.order.push_back(frame.key.clone());
self.entries.insert(frame.key.clone(), frame);
Ok(evicted)
}
pub fn sweep_retention(
&mut self,
now_tick: u64,
adapt_hz: u16,
windows: &[RetentionWindow],
) -> Vec<StoreEvicted> {
let expired: Vec<StoreKey> = self
.entries
.values()
.filter(|frame| {
windows
.iter()
.find(|window| {
window.session == frame.session && window.receipt_seq == frame.receipt_seq
})
.is_none_or(|window| {
elapsed_ms(now_tick, frame.inserted_tick, adapt_hz) > window.retain_ms
})
})
.map(|frame| frame.key.clone())
.collect();
expired
.into_iter()
.filter_map(|key| {
self.remove_existing(&key).map(|_| StoreEvicted {
key,
reason: retention_reason(),
})
})
.collect()
}
pub fn contains(&self, key: &StoreKey) -> bool {
self.entries.contains_key(key)
}
pub fn used_bytes(&self) -> usize {
self.used_bytes
}
pub fn max_bytes(&self) -> usize {
self.max_bytes
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn evict_oldest(&mut self, reason: Symbol) -> Option<StoreEvicted> {
while let Some(key) = self.order.pop_front() {
if self.remove_existing(&key).is_some() {
return Some(StoreEvicted { key, reason });
}
}
None
}
fn remove_existing(&mut self, key: &StoreKey) -> Option<ContentFrame> {
let frame = self.entries.remove(key)?;
self.used_bytes = self.used_bytes.saturating_sub(frame.size_bytes);
self.order.retain(|existing| existing != key);
Some(frame)
}
}
pub fn size_bound_reason() -> Symbol {
Symbol::qualified("stream/content-evict", "size-bound")
}
pub fn retention_reason() -> Symbol {
Symbol::qualified("stream/content-evict", "retention")
}
fn replaced_reason() -> Symbol {
Symbol::qualified("stream/content-evict", "replaced")
}
fn elapsed_ms(now_tick: u64, then_tick: u64, adapt_hz: u16) -> u64 {
let elapsed_ticks = now_tick.saturating_sub(then_tick);
elapsed_ticks.saturating_mul(1000) / u64::from(adapt_hz.max(1))
}