use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::sync::Mutex;
use std::time::SystemTime;
use zerodds_qos::policies::history::HistoryKind;
use crate::contract::Contract;
use crate::error::{Result, StoreError};
use crate::model::{Cursor, DurabilitySample, Page, Selector, StoreStats};
use crate::store::{DEFAULT_PAGE, DurabilityStore};
type Key = (String, [u8; 16]);
#[derive(Default)]
struct Slot {
samples: Vec<DurabilitySample>,
unregistered_at: Option<SystemTime>,
}
#[derive(Default)]
struct Inner {
default_contract: Contract,
contracts: BTreeMap<String, Contract>,
by_key: BTreeMap<Key, Slot>,
}
impl Inner {
fn contract_for(&self, topic: &str) -> Contract {
self.contracts
.get(topic)
.copied()
.unwrap_or(self.default_contract)
}
fn topic_range(topic: &str) -> (Key, Key) {
(
(topic.to_string(), [0u8; 16]),
(topic.to_string(), [0xffu8; 16]),
)
}
fn topic_sample_count(&self, topic: &str) -> usize {
let (lo, hi) = Self::topic_range(topic);
self.by_key
.range(lo..=hi)
.map(|(_, s)| s.samples.len())
.sum()
}
fn topic_instance_count(&self, topic: &str) -> usize {
let (lo, hi) = Self::topic_range(topic);
self.by_key.range(lo..=hi).count()
}
}
#[derive(Default)]
pub struct InMemoryStore {
inner: Mutex<Inner>,
}
impl InMemoryStore {
#[must_use]
pub fn with_default_contract(default_contract: Contract) -> Self {
Self {
inner: Mutex::new(Inner {
default_contract,
..Inner::default()
}),
}
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, Inner>> {
self.inner
.lock()
.map_err(|_| StoreError::Poisoned("in-memory store"))
}
}
fn push_into_slot(slot: &mut Slot, sample: DurabilitySample, contract: &Contract) -> Result<isize> {
match contract.history_kind {
HistoryKind::KeepAll => {
let is_resend = slot
.samples
.binary_search_by(|s| s.sequence.cmp(&sample.sequence))
.is_ok();
if !is_resend
&& contract.per_instance_bounded()
&& slot.samples.len() >= contract.max_samples_per_instance as usize
{
return Err(StoreError::OutOfResources("max_samples_per_instance"));
}
let before = slot.samples.len();
insert_sorted(&mut slot.samples, sample);
Ok(slot.samples.len() as isize - before as isize)
}
HistoryKind::KeepLast => {
let before = slot.samples.len();
insert_sorted(&mut slot.samples, sample);
let depth = contract.effective_depth();
while slot.samples.len() > depth {
slot.samples.remove(0); }
Ok(slot.samples.len() as isize - before as isize)
}
}
}
fn insert_sorted(samples: &mut Vec<DurabilitySample>, sample: DurabilitySample) {
match samples.binary_search_by(|s| s.sequence.cmp(&sample.sequence)) {
Ok(pos) => samples[pos] = sample,
Err(pos) => samples.insert(pos, sample),
}
}
impl DurabilityStore for InMemoryStore {
fn set_contract(&self, topic: &str, contract: Contract) -> Result<()> {
self.lock()?.contracts.insert(topic.to_string(), contract);
Ok(())
}
fn store(&self, sample: DurabilitySample) -> Result<()> {
let mut g = self.lock()?;
let contract = g.contract_for(&sample.topic);
let topic = sample.topic.clone();
let key: Key = (topic.clone(), sample.instance_key);
let is_resend = g
.by_key
.get(&key)
.map(|slot| {
slot.samples
.binary_search_by(|s| s.sequence.cmp(&sample.sequence))
.is_ok()
})
.unwrap_or(false);
if !is_resend
&& contract.samples_bounded()
&& g.topic_sample_count(&topic) >= contract.max_samples as usize
&& matches!(contract.history_kind, HistoryKind::KeepAll)
{
return Err(StoreError::OutOfResources("max_samples"));
}
let new_instance = !g.by_key.contains_key(&key);
if new_instance
&& contract.instances_bounded()
&& g.topic_instance_count(&topic) >= contract.max_instances as usize
{
return Err(StoreError::OutOfResources("max_instances"));
}
let slot = g.by_key.entry(key).or_default();
push_into_slot(slot, sample, &contract)?;
Ok(())
}
fn query(&self, topic: &str, selector: &Selector) -> Result<Page> {
let g = self.lock()?;
let (lo, hi) = Inner::topic_range(topic);
let mut matched: Vec<DurabilitySample> = g
.by_key
.range(lo..=hi)
.flat_map(|(_, s)| s.samples.iter())
.filter(|s| selector.matches(s))
.cloned()
.collect();
matched.sort_by_key(|s| (s.instance_key, s.sequence));
let limit = selector.limit.unwrap_or(DEFAULT_PAGE);
let exhausted = matched.len() <= limit;
matched.truncate(limit);
let next: Option<Cursor> = if exhausted {
None
} else {
matched.last().map(|s| (s.instance_key, s.sequence))
};
Ok(Page {
samples: matched,
next,
})
}
fn unregister(&self, topic: &str, instance_key: &[u8; 16], now: SystemTime) -> Result<()> {
let mut g = self.lock()?;
if let Some(slot) = g.by_key.get_mut(&(topic.to_string(), *instance_key)) {
slot.unregistered_at = Some(now);
}
Ok(())
}
fn cleanup(&self, now: SystemTime) -> Result<usize> {
let mut g = self.lock()?;
let due: Vec<Key> = g
.by_key
.iter()
.filter_map(|(k, slot)| {
let ts = slot.unregistered_at?;
let delay = g.contract_for(&k.0).cleanup_delay;
let deadline = ts.checked_add(delay)?;
if now >= deadline {
Some(k.clone())
} else {
None
}
})
.collect();
let removed = due.len();
for k in due {
g.by_key.remove(&k);
}
Ok(removed)
}
fn stats(&self, topic: &str) -> Result<StoreStats> {
let g = self.lock()?;
let (lo, hi) = Inner::topic_range(topic);
let mut stats = StoreStats::default();
for (_, slot) in g.by_key.range(lo..=hi) {
stats.instances += 1;
stats.samples += slot.samples.len();
stats.bytes += slot
.samples
.iter()
.map(|s| s.payload.len() as u64)
.sum::<u64>();
}
Ok(stats)
}
}