nodedb_lite/nodedb/collection/
kv.rs1use nodedb_types::Namespace;
21use nodedb_types::error::{NodeDbError, NodeDbResult};
22
23use super::super::{LockExt, NodeDbLite};
24use crate::storage::engine::{StorageEngine, WriteOp};
25
26pub(super) const KV_FLUSH_THRESHOLD: usize = 1024;
28
29const DEADLINE_PREFIX_LEN: usize = 8;
31
32pub(super) fn kv_key(collection: &str, key: &[u8]) -> Vec<u8> {
34 let mut k = Vec::with_capacity(collection.len() + 1 + key.len());
35 k.extend_from_slice(collection.as_bytes());
36 k.push(0);
37 k.extend_from_slice(key);
38 k
39}
40
41pub(super) fn split_kv_key(composite: &[u8]) -> Option<(&str, &[u8])> {
43 let sep = composite.iter().position(|&b| b == 0)?;
44 let coll = std::str::from_utf8(&composite[..sep]).ok()?;
45 let key = &composite[sep + 1..];
46 Some((coll, key))
47}
48
49pub(super) fn encode_value(deadline_ms: u64, value: &[u8]) -> Vec<u8> {
53 let mut encoded = Vec::with_capacity(DEADLINE_PREFIX_LEN + value.len());
54 encoded.extend_from_slice(&deadline_ms.to_le_bytes());
55 encoded.extend_from_slice(value);
56 encoded
57}
58
59pub(super) fn decode_value(stored: &[u8]) -> Option<(u64, &[u8])> {
63 if stored.len() < DEADLINE_PREFIX_LEN {
64 return None;
65 }
66 let deadline = u64::from_le_bytes(stored[..DEADLINE_PREFIX_LEN].try_into().ok()?);
67 Some((deadline, &stored[DEADLINE_PREFIX_LEN..]))
68}
69
70pub(super) fn is_expired(deadline_ms: u64) -> bool {
74 deadline_ms != 0 && crate::runtime::now_millis() >= deadline_ms
75}
76
77impl<S: StorageEngine> NodeDbLite<S> {
78 pub async fn kv_put(&self, collection: &str, key: &str, value: &[u8]) -> NodeDbResult<()> {
83 self.kv_put_with_deadline(collection, key, value, 0).await
84 }
85
86 pub async fn kv_put_with_ttl(
91 &self,
92 collection: &str,
93 key: &str,
94 value: &[u8],
95 ttl_ms: u64,
96 ) -> NodeDbResult<()> {
97 let deadline = crate::runtime::now_millis().saturating_add(ttl_ms);
98 self.kv_put_with_deadline(collection, key, value, deadline)
99 .await
100 }
101
102 async fn kv_put_with_deadline(
107 &self,
108 collection: &str,
109 key: &str,
110 value: &[u8],
111 deadline_ms: u64,
112 ) -> NodeDbResult<()> {
113 self.kv_check_pressure()?;
114 let _order = self.kv_local.write_order.lock().await;
115 if self.kv_local.is_indexed(collection) {
116 return self
117 .kv_put_indexed(collection, key, value, deadline_ms)
118 .await;
119 }
120 self.kv_record_put(collection, key.as_bytes(), value, deadline_ms)
121 .await?;
122 self.kv_buffer_put(collection, key, value, deadline_ms)
123 .await
124 }
125
126 async fn kv_put_indexed(
131 &self,
132 collection: &str,
133 key: &str,
134 value: &[u8],
135 deadline_ms: u64,
136 ) -> NodeDbResult<()> {
137 let rkey = kv_key(collection, key.as_bytes());
138 let op = WriteOp::Put {
139 ns: Namespace::Kv,
140 key: rkey.clone(),
141 value: encode_value(deadline_ms, value),
142 };
143 self.kv_local
144 .check(&*self.storage, std::slice::from_ref(&op))
145 .await
146 .map_err(NodeDbError::from)?;
147 self.kv_record_put(collection, key.as_bytes(), value, deadline_ms)
148 .await?;
149 self.kv_flush_inner().await?;
150 self.kv_local
151 .commit(&*self.storage, vec![op])
152 .await
153 .map_err(NodeDbError::from)?;
154 self.kv_local.cache.lock_or_recover().pop(&rkey);
155 Ok(())
156 }
157
158 pub(super) fn kv_check_pressure(&self) -> NodeDbResult<()> {
160 if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
161 return Err(NodeDbError::storage(
162 crate::error::LiteError::Backpressure {
163 detail: "KV write rejected: memory governor is at Emergency pressure".into(),
164 },
165 ));
166 }
167 Ok(())
168 }
169
170 pub(super) async fn kv_buffer_put(
173 &self,
174 collection: &str,
175 key: &str,
176 value: &[u8],
177 deadline_ms: u64,
178 ) -> NodeDbResult<()> {
179 let rkey = kv_key(collection, key.as_bytes());
180 let encoded = encode_value(deadline_ms, value);
181
182 let should_flush = {
184 let mut buf = self.kv_local.write_buf.lock_or_recover();
185 buf.overlay.insert(rkey.clone(), Some(encoded.clone()));
186 buf.ops.push(WriteOp::Put {
187 ns: Namespace::Kv,
188 key: rkey.clone(),
189 value: encoded,
190 });
191 buf.ops.len() >= KV_FLUSH_THRESHOLD
192 };
193
194 {
196 self.kv_local.cache.lock_or_recover().pop(&rkey);
197 }
198
199 if should_flush {
200 self.kv_flush_inner().await?;
201 }
202
203 Ok(())
204 }
205
206 pub async fn kv_get(&self, collection: &str, key: &str) -> NodeDbResult<Option<Vec<u8>>> {
214 let rkey = kv_key(collection, key.as_bytes());
215
216 let overlay_result: Option<Option<Vec<u8>>> = {
221 let buf = self.kv_local.write_buf.lock_or_recover();
222 buf.overlay.get(&rkey).map(|entry| match entry {
223 Some(stored) => decode_value(stored).and_then(|(deadline, user_bytes)| {
224 if is_expired(deadline) {
225 None
226 } else {
227 Some(user_bytes.to_vec())
228 }
229 }),
230 None => None,
231 })
232 };
233 if let Some(result) = overlay_result {
234 return Ok(result);
235 }
236
237 let cache_result: Option<Option<Vec<u8>>> = {
240 let mut cache = self.kv_local.cache.lock_or_recover();
241 if let Some(encoded) = cache.get(&rkey) {
242 match decode_value(encoded) {
243 Some((deadline, user_bytes)) if !is_expired(deadline) => {
244 Some(Some(user_bytes.to_vec()))
245 }
246 _ => {
247 cache.pop(&rkey);
248 None
249 }
250 }
251 } else {
252 None
253 }
254 };
255 if let Some(result) = cache_result {
256 return Ok(result);
257 }
258
259 let stored = self
261 .storage
262 .get(Namespace::Kv, &rkey)
263 .await
264 .map_err(NodeDbError::storage)?;
265
266 match stored {
267 None => Ok(None),
268 Some(raw) => {
269 let decoded = decode_value(&raw);
270 match decoded {
271 None => Ok(None),
272 Some((deadline, user_bytes)) => {
273 if is_expired(deadline) {
274 self.kv_lazy_delete(rkey).await?;
276 Ok(None)
277 } else {
278 let result = user_bytes.to_vec();
279 {
282 self.kv_local.cache.lock_or_recover().put(rkey, raw);
283 }
284 Ok(Some(result))
285 }
286 }
287 }
288 }
289 }
290 }
291
292 async fn kv_lazy_delete(&self, rkey: Vec<u8>) -> NodeDbResult<()> {
294 let should_flush = {
295 let mut buf = self.kv_local.write_buf.lock_or_recover();
296 buf.overlay.insert(rkey.clone(), None);
297 buf.ops.push(WriteOp::Delete {
298 ns: Namespace::Kv,
299 key: rkey.clone(),
300 });
301 buf.ops.len() >= KV_FLUSH_THRESHOLD
302 };
303 {
305 self.kv_local.cache.lock_or_recover().pop(&rkey);
306 }
307 if should_flush {
308 self.kv_flush_inner().await?;
309 }
310 Ok(())
311 }
312
313 pub async fn kv_delete(&self, collection: &str, key: &str) -> NodeDbResult<bool> {
321 let _order = self.kv_local.write_order.lock().await;
322 let existed = self.kv_get(collection, key).await?.is_some();
325 self.kv_record_delete(collection, key.as_bytes()).await?;
326
327 let rkey = kv_key(collection, key.as_bytes());
328
329 let should_flush = {
330 let mut buf = self.kv_local.write_buf.lock_or_recover();
331 buf.overlay.insert(rkey.clone(), None);
332 buf.ops.push(WriteOp::Delete {
333 ns: Namespace::Kv,
334 key: rkey.clone(),
335 });
336 buf.ops.len() >= KV_FLUSH_THRESHOLD
337 };
338
339 {
341 self.kv_local.cache.lock_or_recover().pop(&rkey);
342 }
343
344 if should_flush {
345 self.kv_flush_inner().await?;
346 }
347
348 Ok(existed)
349 }
350
351 pub async fn kv_flush(&self) -> NodeDbResult<usize> {
354 self.kv_flush_inner().await
355 }
356
357 pub(in crate::nodedb) async fn kv_flush_inner(&self) -> NodeDbResult<usize> {
360 self.kv_local
361 .flush_to(&*self.storage)
362 .await
363 .map_err(NodeDbError::storage)
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::config::LiteConfig;
371 use crate::storage::pagedb_storage::PagedbStorageMem;
372
373 async fn open_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
374 let storage = PagedbStorageMem::open_in_memory()
375 .await
376 .expect("open in-memory storage");
377 NodeDbLite::open(storage).await.expect("open NodeDbLite")
378 }
379
380 async fn open_db_with_cache_capacity(
381 cap: usize,
382 ) -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
383 let storage = PagedbStorageMem::open_in_memory()
384 .await
385 .expect("open in-memory storage");
386 let config = LiteConfig {
387 kv_cache_capacity: cap,
388 ..LiteConfig::default()
389 };
390 NodeDbLite::open_with_config(storage, config)
391 .await
392 .expect("open NodeDbLite with config")
393 }
394
395 #[tokio::test]
398 async fn cache_hits_on_repeated_get() {
399 let db = open_db().await;
400 db.kv_put("col", "key", b"hello").await.unwrap();
401 db.kv_flush().await.unwrap();
403 let v1 = db.kv_get("col", "key").await.unwrap();
404 assert_eq!(v1.as_deref(), Some(b"hello".as_ref()));
405 let v2 = db.kv_get("col", "key").await.unwrap();
407 assert_eq!(v2.as_deref(), Some(b"hello".as_ref()));
408 assert_eq!(db.kv_local.cache.lock_or_recover().len(), 1);
410 }
411
412 #[tokio::test]
415 async fn cache_invalidated_on_put() {
416 let db = open_db().await;
417 db.kv_put("col", "key", b"v1").await.unwrap();
418 db.kv_flush().await.unwrap();
419 let _ = db.kv_get("col", "key").await.unwrap(); db.kv_put("col", "key", b"v2").await.unwrap();
421 db.kv_flush().await.unwrap();
422 let v = db.kv_get("col", "key").await.unwrap();
423 assert_eq!(v.as_deref(), Some(b"v2".as_ref()));
424 }
425
426 #[tokio::test]
428 async fn cache_invalidated_on_delete() {
429 let db = open_db().await;
430 db.kv_put("col", "key", b"v").await.unwrap();
431 db.kv_flush().await.unwrap();
432 let _ = db.kv_get("col", "key").await.unwrap(); db.kv_delete("col", "key").await.unwrap();
434 db.kv_flush().await.unwrap();
435 let v = db.kv_get("col", "key").await.unwrap();
436 assert!(v.is_none(), "deleted key must not be returned from cache");
437 }
438
439 #[tokio::test]
441 async fn expired_cached_value_evicted() {
442 let db = open_db().await;
443 db.kv_put_with_ttl("col", "key", b"v", 1).await.unwrap();
445 db.kv_flush().await.unwrap();
446 let _ = db.kv_get("col", "key").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(20)).await;
449 let v = db.kv_get("col", "key").await.unwrap();
450 assert!(v.is_none(), "expired key must return None");
451 assert_eq!(
453 db.kv_local.cache.lock_or_recover().len(),
454 0,
455 "expired entry must be evicted from cache"
456 );
457 }
458
459 #[tokio::test]
461 async fn cache_capacity_eviction() {
462 const CAP: usize = 5;
463 let db = open_db_with_cache_capacity(CAP).await;
464 let col = "cap_test";
465
466 for i in 0..(CAP + 10) {
468 db.kv_put(col, &i.to_string(), b"x").await.unwrap();
469 }
470 db.kv_flush().await.unwrap();
471
472 for i in 0..(CAP + 10) {
474 let _ = db.kv_get(col, &i.to_string()).await.unwrap();
475 }
476
477 let cache_len = db.kv_local.cache.lock_or_recover().len();
478 assert!(
479 cache_len <= CAP,
480 "cache must not exceed capacity {CAP}, got {cache_len}"
481 );
482 }
483}