use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use parking_lot::Mutex;
use rskit_errors::AppResult;
use crate::{Accumulator, AccumulatorConfig, Store};
type StoreFactory<V> = dyn Fn() -> Box<dyn Store<V>> + Send + Sync;
pub struct Manager<K, V>
where
K: Eq + Hash + Clone,
V: Clone + Send + Sync + 'static,
{
accumulators: Mutex<HashMap<K, Arc<Accumulator<V>>>>,
config: AccumulatorConfig<V>,
store_factory: Arc<StoreFactory<V>>,
}
impl<K, V> Manager<K, V>
where
K: Eq + Hash + Clone,
V: Clone + Send + Sync + 'static,
{
#[must_use]
pub fn new(
config: AccumulatorConfig<V>,
store_factory: impl Fn() -> Box<dyn Store<V>> + Send + Sync + 'static,
) -> Self {
Self {
accumulators: Mutex::new(HashMap::new()),
config,
store_factory: Arc::new(store_factory),
}
}
pub fn get_or_create(&self, key: K) -> Arc<Accumulator<V>> {
let mut accumulators = self.accumulators.lock();
Arc::clone(accumulators.entry(key).or_insert_with(|| {
Arc::new(Accumulator::new(
(self.store_factory)(),
self.config.clone(),
))
}))
}
pub fn append(&self, key: K, value: V) -> AppResult<Option<Vec<V>>> {
self.get_or_create(key).append(value)
}
pub fn flush(&self, key: &K) -> AppResult<Option<Vec<V>>> {
match self.accumulators.lock().get(key).cloned() {
Some(accumulator) => Ok(Some(accumulator.flush()?)),
None => Ok(None),
}
}
pub fn size(&self, key: &K) -> AppResult<usize> {
match self.accumulators.lock().get(key).cloned() {
Some(accumulator) => accumulator.size(),
None => Ok(0),
}
}
#[must_use]
pub fn keys(&self) -> Vec<K> {
self.accumulators.lock().keys().cloned().collect()
}
pub fn cleanup_expired(&self) -> AppResult<Vec<K>> {
let snapshot: Vec<(K, Arc<Accumulator<V>>)> = self
.accumulators
.lock()
.iter()
.map(|(key, accumulator)| (key.clone(), Arc::clone(accumulator)))
.collect();
let mut removed = Vec::new();
for (key, accumulator) in snapshot {
if accumulator.is_expired()? {
accumulator.close()?;
self.accumulators.lock().remove(&key);
removed.push(key);
}
}
Ok(removed)
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
use std::time::Duration;
use super::*;
use crate::{AccumulatorConfig, MemoryStore, SizeTrigger};
#[test]
fn manager_routes_values_by_key() {
let manager = Manager::new(AccumulatorConfig::new(), || {
Box::new(MemoryStore::<i32>::new())
});
manager.append("a", 1).unwrap();
manager.append("b", 2).unwrap();
assert_eq!(manager.size(&"a").unwrap(), 1);
assert_eq!(manager.size(&"b").unwrap(), 1);
}
#[tokio::test]
async fn manager_cleans_up_expired_accumulators() {
tokio::time::pause();
let manager = Manager::new(
AccumulatorConfig::new().with_ttl(Duration::from_secs(5)),
|| Box::new(MemoryStore::<i32>::new()),
);
manager.append("a", 1).unwrap();
tokio::time::advance(Duration::from_secs(6)).await;
assert_eq!(manager.cleanup_expired().unwrap(), vec!["a"]);
}
#[test]
fn manager_propagates_flushes() {
let manager = Manager::new(
AccumulatorConfig::new().with_trigger(Arc::new(SizeTrigger::new(2))),
|| Box::new(MemoryStore::<i32>::new()),
);
assert!(manager.append("a", 1).unwrap().is_none());
assert_eq!(manager.append("a", 2).unwrap(), Some(vec![1, 2]));
}
#[test]
fn get_or_create_invokes_factory_once_under_contention() {
const WORKERS: usize = 16;
let factory_calls = Arc::new(AtomicUsize::new(0));
let manager = Arc::new(Manager::new(AccumulatorConfig::new(), {
let factory_calls = Arc::clone(&factory_calls);
move || {
factory_calls.fetch_add(1, Ordering::SeqCst);
Box::new(MemoryStore::<i32>::new())
}
}));
let barrier = Arc::new(Barrier::new(WORKERS));
let threads: Vec<_> = (0..WORKERS)
.map(|_| {
let manager = Arc::clone(&manager);
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
manager.get_or_create("shared")
})
})
.collect();
let accumulators: Vec<_> = threads
.into_iter()
.map(|thread| thread.join().unwrap())
.collect();
assert_eq!(factory_calls.load(Ordering::SeqCst), 1);
for accumulator in &accumulators[1..] {
assert!(Arc::ptr_eq(&accumulators[0], accumulator));
}
}
}