use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::Mutex;
use crate::error::LiteError;
use crate::nodedb::lock_ext::LockExt;
use crate::storage::engine::{StorageEngine, WriteOp};
pub(crate) struct KvWriteBuffer {
pub ops: Vec<WriteOp>,
pub overlay: HashMap<Vec<u8>, Option<Vec<u8>>>,
}
pub struct KvLocalState {
pub(crate) write_buf: Mutex<KvWriteBuffer>,
pub(crate) cache: Mutex<lru::LruCache<Vec<u8>, Vec<u8>>>,
pub(crate) write_order: tokio::sync::Mutex<()>,
indexes: std::sync::OnceLock<std::sync::Arc<crate::index::IndexCatalog>>,
}
impl KvLocalState {
pub(crate) fn new(cache_capacity: NonZeroUsize) -> Self {
Self {
write_buf: Mutex::new(KvWriteBuffer {
ops: Vec::with_capacity(1024),
overlay: HashMap::new(),
}),
cache: Mutex::new(lru::LruCache::new(cache_capacity)),
write_order: tokio::sync::Mutex::new(()),
indexes: std::sync::OnceLock::new(),
}
}
pub(crate) fn set_index_catalog(&self, catalog: std::sync::Arc<crate::index::IndexCatalog>) {
let _ = self.indexes.set(catalog);
}
pub(crate) fn is_indexed(&self, collection: &str) -> bool {
self.indexes
.get()
.is_some_and(|c| c.has_defs(collection, crate::index::IndexEngine::KeyValue))
}
pub(crate) async fn check<S: StorageEngine>(
&self,
storage: &S,
ops: &[WriteOp],
) -> Result<(), LiteError> {
match self.indexes.get() {
Some(catalog) => crate::query::kv_ops::indexes::kv_check(storage, catalog, ops).await,
None => Ok(()),
}
}
pub(crate) async fn commit<S: StorageEngine>(
&self,
storage: &S,
ops: Vec<WriteOp>,
) -> Result<(), LiteError> {
match self.indexes.get() {
Some(catalog) => crate::query::kv_ops::indexes::kv_commit(storage, catalog, ops).await,
None if ops.is_empty() => Ok(()),
None => storage.batch_write(&ops).await,
}
}
pub(crate) async fn flush_to<S: StorageEngine>(&self, storage: &S) -> Result<usize, LiteError> {
let ops: Vec<WriteOp> = {
let mut buf = self.write_buf.lock_or_recover();
if buf.ops.is_empty() {
return Ok(0);
}
let ops = std::mem::take(&mut buf.ops);
buf.overlay.clear();
ops
};
let count = ops.len();
self.commit(storage, ops).await?;
Ok(count)
}
pub(crate) fn forget_collection(&self, collection: &str) -> usize {
let mut prefix = collection.as_bytes().to_vec();
prefix.push(0);
let dropped = {
let mut buf = self.write_buf.lock_or_recover();
let before = buf.ops.len();
buf.ops.retain(|op| {
let key = match op {
WriteOp::Put { key, .. } | WriteOp::Delete { key, .. } => key,
};
!key.starts_with(&prefix)
});
buf.overlay.retain(|key, _| !key.starts_with(&prefix));
before - buf.ops.len()
};
let mut cache = self.cache.lock_or_recover();
let stale: Vec<Vec<u8>> = cache
.iter()
.filter(|(key, _)| key.starts_with(&prefix))
.map(|(key, _)| key.clone())
.collect();
for key in stale {
cache.pop(&key);
}
dropped
}
}
#[cfg(test)]
mod tests {
use nodedb_types::Namespace;
use super::*;
fn key(collection: &str, k: &str) -> Vec<u8> {
let mut key = collection.as_bytes().to_vec();
key.push(0);
key.extend_from_slice(k.as_bytes());
key
}
#[test]
fn forget_collection_drops_only_that_collection() {
let state = KvLocalState::new(NonZeroUsize::new(8).expect("cap"));
{
let mut buf = state.write_buf.lock_or_recover();
for (c, k) in [("a", "1"), ("a", "2"), ("b", "1")] {
buf.ops.push(WriteOp::Put {
ns: Namespace::Kv,
key: key(c, k),
value: vec![1],
});
buf.overlay.insert(key(c, k), Some(vec![1]));
}
buf.ops.push(WriteOp::Delete {
ns: Namespace::Kv,
key: key("a", "3"),
});
}
{
let mut cache = state.cache.lock_or_recover();
cache.put(key("a", "1"), vec![1]);
cache.put(key("b", "1"), vec![1]);
}
assert_eq!(state.forget_collection("a"), 3);
let buf = state.write_buf.lock_or_recover();
assert_eq!(buf.ops.len(), 1);
assert_eq!(buf.overlay.len(), 1);
assert!(buf.overlay.contains_key(&key("b", "1")));
let cache = state.cache.lock_or_recover();
assert_eq!(cache.len(), 1);
assert!(cache.peek(&key("b", "1")).is_some());
}
}