use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use std::sync::Mutex;
use std::time::SystemTime;
use crate::contract::Contract;
use crate::error::{Result, StoreError};
use crate::model::{DurabilitySample, Page, Selector, StoreStats};
use crate::store::DurabilityStore;
type HotKey = (u64, [u8; 16]);
#[derive(Default)]
struct HotTopic {
samples: BTreeMap<HotKey, DurabilitySample>,
bytes: u64,
}
#[derive(Default)]
struct Hot {
topics: BTreeMap<String, HotTopic>,
}
pub struct TieredStore<C: DurabilityStore> {
cold: C,
hot: Mutex<Hot>,
budget_bytes: u64,
}
impl<C: DurabilityStore> TieredStore<C> {
#[must_use]
pub fn new(cold: C, budget_bytes: u64) -> Self {
Self {
cold,
hot: Mutex::new(Hot::default()),
budget_bytes,
}
}
pub fn cold(&self) -> &C {
&self.cold
}
fn lock_hot(&self) -> Result<std::sync::MutexGuard<'_, Hot>> {
self.hot
.lock()
.map_err(|_| StoreError::Poisoned("tiered hot cache"))
}
fn cache(&self, sample: &DurabilitySample) -> Result<()> {
if self.budget_bytes == 0 {
return Ok(());
}
let mut g = self.lock_hot()?;
let topic = g.topics.entry(sample.topic.clone()).or_default();
let hk: HotKey = (sample.sequence, sample.instance_key);
if let Some(old) = topic.samples.insert(hk, sample.clone()) {
topic.bytes = topic.bytes.saturating_sub(old.payload.len() as u64);
}
topic.bytes += sample.payload.len() as u64;
while topic.bytes > self.budget_bytes {
let Some((&k, _)) = topic.samples.iter().next() else {
break;
};
if let Some(removed) = topic.samples.remove(&k) {
topic.bytes = topic.bytes.saturating_sub(removed.payload.len() as u64);
}
if topic.samples.len() <= 1 {
break;
}
}
Ok(())
}
pub fn recent(&self, topic: &str, selector: &Selector) -> Result<Vec<DurabilitySample>> {
let g = self.lock_hot()?;
let Some(t) = g.topics.get(topic) else {
return Ok(Vec::new());
};
let mut out: Vec<DurabilitySample> = t
.samples
.values()
.filter(|s| selector.matches(s))
.cloned()
.collect();
out.sort_by_key(|s| (s.instance_key, s.sequence));
Ok(out)
}
pub fn hot_bytes(&self, topic: &str) -> Result<u64> {
Ok(self
.lock_hot()?
.topics
.get(topic)
.map(|t| t.bytes)
.unwrap_or(0))
}
}
impl<C: DurabilityStore> DurabilityStore for TieredStore<C> {
fn set_contract(&self, topic: &str, contract: Contract) -> Result<()> {
self.cold.set_contract(topic, contract)
}
fn store(&self, sample: DurabilitySample) -> Result<()> {
self.cold.store(sample.clone())?;
self.cache(&sample)
}
fn query(&self, topic: &str, selector: &Selector) -> Result<Page> {
self.cold.query(topic, selector)
}
fn unregister(&self, topic: &str, instance_key: &[u8; 16], now: SystemTime) -> Result<()> {
self.cold.unregister(topic, instance_key, now)?;
if let Ok(mut g) = self.hot.lock() {
if let Some(t) = g.topics.get_mut(topic) {
let drop: Vec<HotKey> = t
.samples
.keys()
.filter(|(_, k)| k == instance_key)
.copied()
.collect();
for k in drop {
if let Some(removed) = t.samples.remove(&k) {
t.bytes = t.bytes.saturating_sub(removed.payload.len() as u64);
}
}
}
}
Ok(())
}
fn cleanup(&self, now: SystemTime) -> Result<usize> {
self.cold.cleanup(now)
}
fn stats(&self, topic: &str) -> Result<StoreStats> {
self.cold.stats(topic)
}
}