diff --git a/Cargo.lock b/Cargo.lock index 6048c8c..ff64aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "sparkv" -version = "0.1.1" +version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 047d4fd..73dfcfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sparkv" authors = ["U-Zyn Chua "] -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "Expirable in-memory key-value store" keywords = ["key-value", "database", "embedded-database", "ttl", "in-memory"] diff --git a/README.md b/README.md index d25ea22..25eb326 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SparKV is an expirable in-memory key-value store for Rust. 1. Flexible expiration duration (a.k.a. time-to-live or TTL) per entry instead of database-wide common TTL. 1. This is similar to that of DNS where each entries of the same domain can have its own unique TTL. 2. Automatically clears expired entries by default. -3. String-based key-value store. +3. Generic over the value type, defaulting to `String`. Store any type you like. 4. Fast data entry enforcements, including ensuring entry size, database size and max TTL. 5. SparKV is intentionally not an LRU cache. 6. Configurable. @@ -25,21 +25,103 @@ Quick start ```rust use sparkv::SparKV; +// The value type defaults to String. let mut sparkv = SparKV::new(); -sparkv.set("your-key", "your-value"); // write +sparkv.set("your-key", "your-value".to_string()).unwrap(); // write let value = sparkv.get("your-key").unwrap(); // read // Write with unique TTL -sparkv.set_with_ttl("diff-ttl", "your-value", std::time::Duration::from_secs(60)); +sparkv.set_with_ttl("diff-ttl", "your-value".to_string(), std::time::Duration::from_secs(60)).unwrap(); ``` -See `config.rs` for more configuration options. +`set` takes the value by value, and `get` returns an owned clone (so the value type +must implement `Clone`). + +### Generic value types + +SparKV is generic over the value type. Built-in types (integers, `Vec`, etc.) +work out of the box: + +```rust +use sparkv::SparKV; + +let mut counters: SparKV = SparKV::new(); +counters.set("hits", 42).unwrap(); +assert_eq!(counters.get("hits"), Some(42)); +``` + +To store your own type, implement `ValueSize` so SparKV can enforce `max_item_size` +(return `0` if you don't care about size enforcement): + +```rust +use sparkv::{SparKV, ValueSize}; + +#[derive(Clone)] +struct Session { + user_id: u64, + csrf: String, +} + +impl ValueSize for Session { + fn value_size(&self) -> usize { + std::mem::size_of::() + self.csrf.len() + } +} + +let mut sessions: SparKV = SparKV::new(); +sessions + .set_with_ttl( + "sid", + Session { user_id: 1, csrf: "abc".to_string() }, + std::time::Duration::from_secs(60), + ) + .unwrap(); +``` + +See `config.rs` for more configuration options. `max_item_size` is an +`Option` — set it to `None` to disable per-entry size enforcement. + + +## Migrating from 0.1 to 0.2 + +`0.2.0` makes the value type generic (`SparKV`, defaulting to `String`). The store +behaves exactly as before; the breaking changes are limited to how values are passed in +and how `max_item_size` is configured. The `SparKV` and `KvEntry` type names still +resolve to their `String` forms, so type annotations and struct fields are unaffected. + +**1. `set` / `set_with_ttl` now take the value by value.** Pass an owned value instead +of a `&str`: + +```rust +// 0.1 +kv.set("key", "value"); +// 0.2 +kv.set("key", "value".to_string()); // or "value".into() +``` + +`get` and `delete` still return `Option` for the default `String` store, so +your read sites do not change. + +**2. `Config.max_item_size` is now `Option`.** Wrap the limit in `Some`, or use +`None` to disable size enforcement: + +```rust +// 0.1 +config.max_item_size = 500_000; +// 0.2 +config.max_item_size = Some(500_000); // or None to disable +``` + +**3. Custom value types need a `ValueSize` impl** — only relevant if you switch away from +`String`. Built-in types (integers, `Vec`, `&[u8]`, etc.) already implement it; see +the generic example above. + +No other call sites change. ## TODO 1. Documentations -1. Support generic data types ## License diff --git a/src/config.rs b/src/config.rs index cc3f6d1..67c8eb1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ #[derive(Debug, PartialEq, Clone, Copy)] pub struct Config { pub max_items: usize, - pub max_item_size: usize, + pub max_item_size: Option, pub max_ttl: std::time::Duration, pub default_ttl: std::time::Duration, pub auto_clear_expired: bool, @@ -11,7 +11,7 @@ impl Config { pub fn new() -> Self { Config { max_items: 10_000, - max_item_size: 500_000, + max_item_size: Some(500_000), max_ttl: std::time::Duration::from_secs(60 * 60), default_ttl: std::time::Duration::from_secs(5 * 60), // 5 minutes auto_clear_expired: true, @@ -33,9 +33,9 @@ mod tests { fn test_new() { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); - assert_eq!(config.max_item_size, 500_000); + assert_eq!(config.max_item_size, Some(500_000)); assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); assert_eq!(config.default_ttl, std::time::Duration::from_secs(5 * 60)); - assert_eq!(config.auto_clear_expired, true); + assert!(config.auto_clear_expired); } } diff --git a/src/expentry.rs b/src/expentry.rs index 5a0759c..77f7547 100644 --- a/src/expentry.rs +++ b/src/expentry.rs @@ -15,7 +15,7 @@ impl ExpEntry { } } - pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { + pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { Self { key: kv_entry.key.clone(), expired_at: kv_entry.expired_at, @@ -60,7 +60,7 @@ mod tests { fn test_from_kventry() { let kv_entry = KvEntry::new( "keyFromKV", - "value from KV", + String::from("value from KV"), std::time::Duration::from_secs(10), ); let exp_item = ExpEntry::from_kv_entry(&kv_entry); diff --git a/src/kventry.rs b/src/kventry.rs index a1e0a0e..e168605 100644 --- a/src/kventry.rs +++ b/src/kventry.rs @@ -1,16 +1,16 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct KvEntry { +#[derive(Debug, Clone)] +pub struct KvEntry { pub key: String, - pub value: String, + pub value: V, pub expired_at: std::time::Instant, } -impl KvEntry { - pub fn new(key: &str, value: &str, expiration: std::time::Duration) -> Self { +impl KvEntry { + pub fn new(key: &str, value: V, expiration: std::time::Duration) -> Self { let expired_at: std::time::Instant = std::time::Instant::now() + expiration; Self { key: String::from(key), - value: String::from(value), + value, expired_at, } } @@ -22,7 +22,11 @@ mod tests { #[test] fn test_new() { - let item = KvEntry::new("key", "value", std::time::Duration::from_secs(10)); + let item = KvEntry::new( + "key", + String::from("value"), + std::time::Duration::from_secs(10), + ); assert_eq!(item.key, "key"); assert_eq!(item.value, "value"); assert!(item.expired_at > std::time::Instant::now() + std::time::Duration::from_secs(9)); diff --git a/src/lib.rs b/src/lib.rs index 72db209..e48d2c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,19 +2,21 @@ mod config; mod error; mod expentry; mod kventry; +mod value_size; pub use config::Config; pub use error::Error; pub use expentry::ExpEntry; pub use kventry::KvEntry; +pub use value_size::ValueSize; -pub struct SparKV { +pub struct SparKV { pub config: Config, - data: std::collections::HashMap, + data: std::collections::HashMap>, expiries: std::collections::BinaryHeap, } -impl SparKV { +impl SparKV { pub fn new() -> Self { let config = Config::new(); SparKV::with_config(config) @@ -28,36 +30,8 @@ impl SparKV { } } - pub fn set(&mut self, key: &str, value: &str) -> Result<(), Error> { - self.set_with_ttl(key, value, self.config.default_ttl) - } - - pub fn set_with_ttl( - &mut self, - key: &str, - value: &str, - ttl: std::time::Duration, - ) -> Result<(), Error> { - self.clear_expired_if_auto(); - self.ensure_capacity_ignore_key(key)?; - self.ensure_item_size(value)?; - self.ensure_max_ttl(ttl)?; - - let item: KvEntry = KvEntry::new(key, value, ttl); - let exp_item: ExpEntry = ExpEntry::from_kv_entry(&item); - - self.expiries.push(exp_item); - self.data.insert(item.key.clone(), item); - Ok(()) - } - - pub fn get(&self, key: &str) -> Option { - let item = self.get_item(key)?; - Some(item.value.clone()) - } - // Only returns if it is not yet expired - pub fn get_item(&self, key: &str) -> Option<&KvEntry> { + pub fn get_item(&self, key: &str) -> Option<&KvEntry> { let item = self.data.get(key)?; if item.expired_at > std::time::Instant::now() { Some(item) @@ -66,7 +40,7 @@ impl SparKV { } } - pub fn delete(&mut self, key: &str) -> Option { + pub fn delete(&mut self, key: &str) -> Option { self.clear_expired_if_auto(); let item = self.data.remove(key)?; // Does not delete from BinaryHeap as it's expensive. @@ -130,22 +104,56 @@ impl SparKV { self.ensure_capacity() } - fn ensure_item_size(&self, value: &str) -> Result<(), Error> { - if value.len() > self.config.max_item_size { - return Err(Error::ItemSizeExceeded); + fn ensure_max_ttl(&self, ttl: std::time::Duration) -> Result<(), Error> { + if ttl > self.config.max_ttl { + return Err(Error::TTLTooLong); } Ok(()) } +} - fn ensure_max_ttl(&self, ttl: std::time::Duration) -> Result<(), Error> { - if ttl > self.config.max_ttl { - return Err(Error::TTLTooLong); +impl SparKV { + pub fn set(&mut self, key: &str, value: V) -> Result<(), Error> { + self.set_with_ttl(key, value, self.config.default_ttl) + } + + pub fn set_with_ttl( + &mut self, + key: &str, + value: V, + ttl: std::time::Duration, + ) -> Result<(), Error> { + self.clear_expired_if_auto(); + self.ensure_capacity_ignore_key(key)?; + self.ensure_item_size(&value)?; + self.ensure_max_ttl(ttl)?; + + let item: KvEntry = KvEntry::new(key, value, ttl); + let exp_item: ExpEntry = ExpEntry::from_kv_entry(&item); + + self.expiries.push(exp_item); + self.data.insert(item.key.clone(), item); + Ok(()) + } + + fn ensure_item_size(&self, value: &V) -> Result<(), Error> { + if let Some(max_item_size) = self.config.max_item_size { + if value.value_size() > max_item_size { + return Err(Error::ItemSizeExceeded); + } } Ok(()) } } -impl Default for SparKV { +impl SparKV { + pub fn get(&self, key: &str) -> Option { + let item = self.get_item(key)?; + Some(item.value.clone()) + } +} + +impl Default for SparKV { fn default() -> Self { Self::new() } @@ -159,14 +167,14 @@ mod tests { fn test_sparkv_config() { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); - assert_eq!(config.max_item_size, 500_000); + assert_eq!(config.max_item_size, Some(500_000)); assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); } #[test] fn test_sparkv_new_with_config() { let config: Config = Config::new(); - let sparkv = SparKV::with_config(config.clone()); + let sparkv: SparKV = SparKV::with_config(config); assert_eq!(sparkv.config, config); } @@ -174,32 +182,71 @@ mod tests { fn test_len_is_empty() { let mut sparkv = SparKV::new(); assert_eq!(sparkv.len(), 0); - assert_eq!(sparkv.is_empty(), true); + assert!(sparkv.is_empty()); - _ = sparkv.set("keyA", "value"); + _ = sparkv.set("keyA", String::from("value")); assert_eq!(sparkv.len(), 1); - assert_eq!(sparkv.is_empty(), false); + assert!(!sparkv.is_empty()); } #[test] fn test_set_get() { let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); + _ = sparkv.set("keyA", String::from("value")); assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); assert_eq!(sparkv.expiries.len(), 1); // Overwrite the value - _ = sparkv.set("keyA", "value2"); + _ = sparkv.set("keyA", String::from("value2")); assert_eq!(sparkv.get("keyA"), Some(String::from("value2"))); assert_eq!(sparkv.expiries.len(), 2); assert!(sparkv.get("non-existent").is_none()); } + #[test] + fn test_generic_value_type() { + let mut sparkv: SparKV = SparKV::new(); + _ = sparkv.set("answer", 42); + assert_eq!(sparkv.get("answer"), Some(42)); + + _ = sparkv.set("answer", -7); + assert_eq!(sparkv.get("answer"), Some(-7)); + + assert_eq!(sparkv.delete("answer"), Some(-7)); + assert!(sparkv.get("answer").is_none()); + } + + #[test] + fn test_generic_item_size_enforced() { + let mut config: Config = Config::new(); + config.max_item_size = Some(4); // i64 is 8 bytes + let mut sparkv: SparKV = SparKV::with_config(config); + + let set_result = sparkv.set("too-big", 1); + assert_eq!(set_result.unwrap_err(), Error::ItemSizeExceeded); + } + + #[test] + fn test_unbounded_item_size_when_none() { + let mut config: Config = Config::new(); + config.max_item_size = None; + let mut sparkv = SparKV::with_config(config); + + let huge = "x".repeat(1_000_000); + let set_result = sparkv.set("huge", huge.clone()); + assert!(set_result.is_ok()); + assert_eq!(sparkv.get("huge"), Some(huge)); + } + #[test] fn test_get_item() { let mut sparkv = SparKV::new(); - let item = KvEntry::new("keyARaw", "value99", std::time::Duration::from_secs(1)); + let item = KvEntry::new( + "keyARaw", + String::from("value99"), + std::time::Duration::from_secs(1), + ); sparkv.data.insert(item.key.clone(), item); let get_result = sparkv.get_item("keyARaw"); let unwrapped = get_result.unwrap(); @@ -214,7 +261,11 @@ mod tests { #[test] fn test_get_item_return_none_if_expired() { let mut sparkv = SparKV::new(); - _ = sparkv.set_with_ttl("kkk", "value", std::time::Duration::from_millis(50)); + _ = sparkv.set_with_ttl( + "kkk", + String::from("value"), + std::time::Duration::from_millis(50), + ); assert_eq!(sparkv.get("kkk"), Some(String::from("value"))); std::thread::sleep(std::time::Duration::from_millis(60)); @@ -227,20 +278,20 @@ mod tests { config.max_items = 2; let mut sparkv = SparKV::with_config(config); - let mut set_result = sparkv.set("keyA", "value"); + let mut set_result = sparkv.set("keyA", String::from("value")); assert!(set_result.is_ok()); assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - set_result = sparkv.set("keyB", "value2"); + set_result = sparkv.set("keyB", String::from("value2")); assert!(set_result.is_ok()); - set_result = sparkv.set("keyC", "value3"); + set_result = sparkv.set("keyC", String::from("value3")); assert!(set_result.is_err()); assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded); assert!(sparkv.get("keyC").is_none()); // Overwrite existing key should not err - set_result = sparkv.set("keyB", "newValue1234"); + set_result = sparkv.set("keyB", String::from("newValue1234")); assert!(set_result.is_ok()); assert_eq!(sparkv.get("keyB"), Some(String::from("newValue1234"))); } @@ -248,9 +299,17 @@ mod tests { #[test] fn test_set_with_ttl() { let mut sparkv = SparKV::new(); - _ = sparkv.set("longest", "value"); - _ = sparkv.set_with_ttl("longer", "value", std::time::Duration::from_secs(2)); - _ = sparkv.set_with_ttl("shorter", "value", std::time::Duration::from_secs(1)); + _ = sparkv.set("longest", String::from("value")); + _ = sparkv.set_with_ttl( + "longer", + String::from("value"), + std::time::Duration::from_secs(2), + ); + _ = sparkv.set_with_ttl( + "shorter", + String::from("value"), + std::time::Duration::from_secs(1), + ); assert_eq!(sparkv.get("longer"), Some(String::from("value"))); assert_eq!(sparkv.get("shorter"), Some(String::from("value"))); @@ -271,20 +330,30 @@ mod tests { config.default_ttl = std::time::Duration::from_secs(5000); let mut sparkv = SparKV::with_config(config); - let set_result_long_def = sparkv.set("default is longer than max", "should fail"); + let set_result_long_def = + sparkv.set("default is longer than max", String::from("should fail")); assert!(set_result_long_def.is_err()); assert_eq!(set_result_long_def.unwrap_err(), Error::TTLTooLong); - let set_result_ok = - sparkv.set_with_ttl("shorter", "ok", std::time::Duration::from_secs(3599)); + let set_result_ok = sparkv.set_with_ttl( + "shorter", + String::from("ok"), + std::time::Duration::from_secs(3599), + ); assert!(set_result_ok.is_ok()); - let set_result_ok_2 = - sparkv.set_with_ttl("exact", "ok", std::time::Duration::from_secs(3600)); + let set_result_ok_2 = sparkv.set_with_ttl( + "exact", + String::from("ok"), + std::time::Duration::from_secs(3600), + ); assert!(set_result_ok_2.is_ok()); - let set_result_not_ok = - sparkv.set_with_ttl("not", "not ok", std::time::Duration::from_secs(3601)); + let set_result_not_ok = sparkv.set_with_ttl( + "not", + String::from("not ok"), + std::time::Duration::from_secs(3601), + ); assert!(set_result_not_ok.is_err()); assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); } @@ -292,7 +361,7 @@ mod tests { #[test] fn test_delete() { let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); + _ = sparkv.set("keyA", String::from("value")); assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); assert_eq!(sparkv.expiries.len(), 1); @@ -307,9 +376,21 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("not-yet-expired", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("expiring", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "not-yet-expired", + String::from("v"), + std::time::Duration::from_secs(90), + ); + _ = sparkv.set_with_ttl( + "expiring", + String::from("value"), + std::time::Duration::from_millis(1), + ); + _ = sparkv.set_with_ttl( + "not-expired", + String::from("value"), + std::time::Duration::from_secs(60), + ); std::thread::sleep(std::time::Duration::from_millis(2)); assert_eq!(sparkv.len(), 3); @@ -325,9 +406,21 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "no-longer", + String::from("value"), + std::time::Duration::from_millis(1), + ); + _ = sparkv.set_with_ttl( + "no-longer", + String::from("v"), + std::time::Duration::from_secs(90), + ); + _ = sparkv.set_with_ttl( + "not-expired", + String::from("value"), + std::time::Duration::from_secs(60), + ); std::thread::sleep(std::time::Duration::from_millis(2)); assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries assert_eq!(sparkv.len(), 2); @@ -343,15 +436,31 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = true; // explicitly setting it to true let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); + _ = sparkv.set_with_ttl( + "no-longer", + String::from("value"), + std::time::Duration::from_millis(1), + ); + _ = sparkv.set_with_ttl( + "no-longer", + String::from("v"), + std::time::Duration::from_secs(90), + ); std::thread::sleep(std::time::Duration::from_millis(2)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "not-expired", + String::from("value"), + std::time::Duration::from_secs(60), + ); assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear assert_eq!(sparkv.len(), 2); // auto clear - _ = sparkv.set_with_ttl("new-", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "new-", + String::from("value"), + std::time::Duration::from_secs(60), + ); assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries assert_eq!(sparkv.len(), 3); // but not actually deleting } diff --git a/src/value_size.rs b/src/value_size.rs new file mode 100644 index 0000000..ab73f1a --- /dev/null +++ b/src/value_size.rs @@ -0,0 +1,73 @@ +pub trait ValueSize { + /// Logical content size in bytes, compared against `Config::max_item_size`. + fn value_size(&self) -> usize; +} + +impl ValueSize for String { + fn value_size(&self) -> usize { + self.len() + } +} + +impl ValueSize for str { + fn value_size(&self) -> usize { + self.len() + } +} + +impl ValueSize for &str { + fn value_size(&self) -> usize { + self.len() + } +} + +impl ValueSize for Vec { + fn value_size(&self) -> usize { + self.len() + } +} + +impl ValueSize for [u8] { + fn value_size(&self) -> usize { + self.len() + } +} + +impl ValueSize for &[u8] { + fn value_size(&self) -> usize { + self.len() + } +} + +macro_rules! impl_value_size_sized { + ($($t:ty),*) => { + $(impl ValueSize for $t { + fn value_size(&self) -> usize { + std::mem::size_of::<$t>() + } + })* + }; +} + +impl_value_size_sized!( + bool, char, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_str_like_size() { + assert_eq!(String::from("abc").value_size(), 3); + assert_eq!("hello".value_size(), 5); + assert_eq!(vec![1u8, 2, 3, 4].value_size(), 4); + } + + #[test] + fn test_scalar_size() { + assert_eq!(42u32.value_size(), 4); + assert_eq!(42u64.value_size(), 8); + assert_eq!(true.value_size(), 1); + } +}