use nodedb_types::Namespace;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use super::super::{LockExt, NodeDbLite};
use crate::storage::engine::{StorageEngine, WriteOp};
pub(super) const KV_FLUSH_THRESHOLD: usize = 1024;
const DEADLINE_PREFIX_LEN: usize = 8;
pub(super) fn kv_key(collection: &str, key: &[u8]) -> Vec<u8> {
let mut k = Vec::with_capacity(collection.len() + 1 + key.len());
k.extend_from_slice(collection.as_bytes());
k.push(0);
k.extend_from_slice(key);
k
}
pub(super) fn split_kv_key(composite: &[u8]) -> Option<(&str, &[u8])> {
let sep = composite.iter().position(|&b| b == 0)?;
let coll = std::str::from_utf8(&composite[..sep]).ok()?;
let key = &composite[sep + 1..];
Some((coll, key))
}
pub(super) fn encode_value(deadline_ms: u64, value: &[u8]) -> Vec<u8> {
let mut encoded = Vec::with_capacity(DEADLINE_PREFIX_LEN + value.len());
encoded.extend_from_slice(&deadline_ms.to_le_bytes());
encoded.extend_from_slice(value);
encoded
}
pub(super) fn decode_value(stored: &[u8]) -> Option<(u64, &[u8])> {
if stored.len() < DEADLINE_PREFIX_LEN {
return None;
}
let deadline = u64::from_le_bytes(stored[..DEADLINE_PREFIX_LEN].try_into().ok()?);
Some((deadline, &stored[DEADLINE_PREFIX_LEN..]))
}
pub(super) fn is_expired(deadline_ms: u64) -> bool {
deadline_ms != 0 && crate::runtime::now_millis() >= deadline_ms
}
impl<S: StorageEngine> NodeDbLite<S> {
pub async fn kv_put(&self, collection: &str, key: &str, value: &[u8]) -> NodeDbResult<()> {
self.kv_put_with_deadline(collection, key, value, 0).await
}
pub async fn kv_put_with_ttl(
&self,
collection: &str,
key: &str,
value: &[u8],
ttl_ms: u64,
) -> NodeDbResult<()> {
let deadline = crate::runtime::now_millis().saturating_add(ttl_ms);
self.kv_put_with_deadline(collection, key, value, deadline)
.await
}
async fn kv_put_with_deadline(
&self,
collection: &str,
key: &str,
value: &[u8],
deadline_ms: u64,
) -> NodeDbResult<()> {
self.kv_check_pressure()?;
let _order = self.kv_local.write_order.lock().await;
if self.kv_local.is_indexed(collection) {
return self
.kv_put_indexed(collection, key, value, deadline_ms)
.await;
}
self.kv_record_put(collection, key.as_bytes(), value, deadline_ms)
.await?;
self.kv_buffer_put(collection, key, value, deadline_ms)
.await
}
async fn kv_put_indexed(
&self,
collection: &str,
key: &str,
value: &[u8],
deadline_ms: u64,
) -> NodeDbResult<()> {
let rkey = kv_key(collection, key.as_bytes());
let op = WriteOp::Put {
ns: Namespace::Kv,
key: rkey.clone(),
value: encode_value(deadline_ms, value),
};
self.kv_local
.check(&*self.storage, std::slice::from_ref(&op))
.await
.map_err(NodeDbError::from)?;
self.kv_record_put(collection, key.as_bytes(), value, deadline_ms)
.await?;
self.kv_flush_inner().await?;
self.kv_local
.commit(&*self.storage, vec![op])
.await
.map_err(NodeDbError::from)?;
self.kv_local.cache.lock_or_recover().pop(&rkey);
Ok(())
}
pub(super) fn kv_check_pressure(&self) -> NodeDbResult<()> {
if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
return Err(NodeDbError::storage(
crate::error::LiteError::Backpressure {
detail: "KV write rejected: memory governor is at Emergency pressure".into(),
},
));
}
Ok(())
}
pub(super) async fn kv_buffer_put(
&self,
collection: &str,
key: &str,
value: &[u8],
deadline_ms: u64,
) -> NodeDbResult<()> {
let rkey = kv_key(collection, key.as_bytes());
let encoded = encode_value(deadline_ms, value);
let should_flush = {
let mut buf = self.kv_local.write_buf.lock_or_recover();
buf.overlay.insert(rkey.clone(), Some(encoded.clone()));
buf.ops.push(WriteOp::Put {
ns: Namespace::Kv,
key: rkey.clone(),
value: encoded,
});
buf.ops.len() >= KV_FLUSH_THRESHOLD
};
{
self.kv_local.cache.lock_or_recover().pop(&rkey);
}
if should_flush {
self.kv_flush_inner().await?;
}
Ok(())
}
pub async fn kv_get(&self, collection: &str, key: &str) -> NodeDbResult<Option<Vec<u8>>> {
let rkey = kv_key(collection, key.as_bytes());
let overlay_result: Option<Option<Vec<u8>>> = {
let buf = self.kv_local.write_buf.lock_or_recover();
buf.overlay.get(&rkey).map(|entry| match entry {
Some(stored) => decode_value(stored).and_then(|(deadline, user_bytes)| {
if is_expired(deadline) {
None
} else {
Some(user_bytes.to_vec())
}
}),
None => None,
})
};
if let Some(result) = overlay_result {
return Ok(result);
}
let cache_result: Option<Option<Vec<u8>>> = {
let mut cache = self.kv_local.cache.lock_or_recover();
if let Some(encoded) = cache.get(&rkey) {
match decode_value(encoded) {
Some((deadline, user_bytes)) if !is_expired(deadline) => {
Some(Some(user_bytes.to_vec()))
}
_ => {
cache.pop(&rkey);
None
}
}
} else {
None
}
};
if let Some(result) = cache_result {
return Ok(result);
}
let stored = self
.storage
.get(Namespace::Kv, &rkey)
.await
.map_err(NodeDbError::storage)?;
match stored {
None => Ok(None),
Some(raw) => {
let decoded = decode_value(&raw);
match decoded {
None => Ok(None),
Some((deadline, user_bytes)) => {
if is_expired(deadline) {
self.kv_lazy_delete(rkey).await?;
Ok(None)
} else {
let result = user_bytes.to_vec();
{
self.kv_local.cache.lock_or_recover().put(rkey, raw);
}
Ok(Some(result))
}
}
}
}
}
}
async fn kv_lazy_delete(&self, rkey: Vec<u8>) -> NodeDbResult<()> {
let should_flush = {
let mut buf = self.kv_local.write_buf.lock_or_recover();
buf.overlay.insert(rkey.clone(), None);
buf.ops.push(WriteOp::Delete {
ns: Namespace::Kv,
key: rkey.clone(),
});
buf.ops.len() >= KV_FLUSH_THRESHOLD
};
{
self.kv_local.cache.lock_or_recover().pop(&rkey);
}
if should_flush {
self.kv_flush_inner().await?;
}
Ok(())
}
pub async fn kv_delete(&self, collection: &str, key: &str) -> NodeDbResult<bool> {
let _order = self.kv_local.write_order.lock().await;
let existed = self.kv_get(collection, key).await?.is_some();
self.kv_record_delete(collection, key.as_bytes()).await?;
let rkey = kv_key(collection, key.as_bytes());
let should_flush = {
let mut buf = self.kv_local.write_buf.lock_or_recover();
buf.overlay.insert(rkey.clone(), None);
buf.ops.push(WriteOp::Delete {
ns: Namespace::Kv,
key: rkey.clone(),
});
buf.ops.len() >= KV_FLUSH_THRESHOLD
};
{
self.kv_local.cache.lock_or_recover().pop(&rkey);
}
if should_flush {
self.kv_flush_inner().await?;
}
Ok(existed)
}
pub async fn kv_flush(&self) -> NodeDbResult<usize> {
self.kv_flush_inner().await
}
pub(in crate::nodedb) async fn kv_flush_inner(&self) -> NodeDbResult<usize> {
self.kv_local
.flush_to(&*self.storage)
.await
.map_err(NodeDbError::storage)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LiteConfig;
use crate::storage::pagedb_storage::PagedbStorageMem;
async fn open_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory()
.await
.expect("open in-memory storage");
NodeDbLite::open(storage).await.expect("open NodeDbLite")
}
async fn open_db_with_cache_capacity(
cap: usize,
) -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory()
.await
.expect("open in-memory storage");
let config = LiteConfig {
kv_cache_capacity: cap,
..LiteConfig::default()
};
NodeDbLite::open_with_config(storage, config)
.await
.expect("open NodeDbLite with config")
}
#[tokio::test]
async fn cache_hits_on_repeated_get() {
let db = open_db().await;
db.kv_put("col", "key", b"hello").await.unwrap();
db.kv_flush().await.unwrap();
let v1 = db.kv_get("col", "key").await.unwrap();
assert_eq!(v1.as_deref(), Some(b"hello".as_ref()));
let v2 = db.kv_get("col", "key").await.unwrap();
assert_eq!(v2.as_deref(), Some(b"hello".as_ref()));
assert_eq!(db.kv_local.cache.lock_or_recover().len(), 1);
}
#[tokio::test]
async fn cache_invalidated_on_put() {
let db = open_db().await;
db.kv_put("col", "key", b"v1").await.unwrap();
db.kv_flush().await.unwrap();
let _ = db.kv_get("col", "key").await.unwrap(); db.kv_put("col", "key", b"v2").await.unwrap();
db.kv_flush().await.unwrap();
let v = db.kv_get("col", "key").await.unwrap();
assert_eq!(v.as_deref(), Some(b"v2".as_ref()));
}
#[tokio::test]
async fn cache_invalidated_on_delete() {
let db = open_db().await;
db.kv_put("col", "key", b"v").await.unwrap();
db.kv_flush().await.unwrap();
let _ = db.kv_get("col", "key").await.unwrap(); db.kv_delete("col", "key").await.unwrap();
db.kv_flush().await.unwrap();
let v = db.kv_get("col", "key").await.unwrap();
assert!(v.is_none(), "deleted key must not be returned from cache");
}
#[tokio::test]
async fn expired_cached_value_evicted() {
let db = open_db().await;
db.kv_put_with_ttl("col", "key", b"v", 1).await.unwrap();
db.kv_flush().await.unwrap();
let _ = db.kv_get("col", "key").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let v = db.kv_get("col", "key").await.unwrap();
assert!(v.is_none(), "expired key must return None");
assert_eq!(
db.kv_local.cache.lock_or_recover().len(),
0,
"expired entry must be evicted from cache"
);
}
#[tokio::test]
async fn cache_capacity_eviction() {
const CAP: usize = 5;
let db = open_db_with_cache_capacity(CAP).await;
let col = "cap_test";
for i in 0..(CAP + 10) {
db.kv_put(col, &i.to_string(), b"x").await.unwrap();
}
db.kv_flush().await.unwrap();
for i in 0..(CAP + 10) {
let _ = db.kv_get(col, &i.to_string()).await.unwrap();
}
let cache_len = db.kv_local.cache.lock_or_recover().len();
assert!(
cache_len <= CAP,
"cache must not exceed capacity {CAP}, got {cache_len}"
);
}
}