diff --git a/Cargo.lock b/Cargo.lock index 1bc24ff4..4c84f84b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,7 +769,9 @@ version = "1.0.0" dependencies = [ "anyhow", "base64", + "bd-bonjson", "bd-log", + "bd-resilient-kv", "bd-test-helpers", "bd-workspace-hack", "bincode 2.0.1", diff --git a/bd-key-value/Cargo.toml b/bd-key-value/Cargo.toml index 438aa30a..c00e7d85 100644 --- a/bd-key-value/Cargo.toml +++ b/bd-key-value/Cargo.toml @@ -11,10 +11,13 @@ doctest = false [dependencies] anyhow.workspace = true base64.workspace = true +bd-bonjson.path = "../bd-bonjson" bd-log.path = "../bd-log" +bd-resilient-kv.path = "../bd-resilient-kv" bd-workspace-hack.workspace = true bincode.workspace = true log.workspace = true +parking_lot.workspace = true serde.workspace = true time.workspace = true tokio.workspace = true diff --git a/bd-key-value/src/lib.rs b/bd-key-value/src/lib.rs index 21a1f426..e426dc12 100644 --- a/bd-key-value/src/lib.rs +++ b/bd-key-value/src/lib.rs @@ -162,3 +162,6 @@ where self.key } } + +pub mod resilient_kv; +pub use resilient_kv::ResilientKvStorage; diff --git a/bd-key-value/src/resilient_kv.rs b/bd-key-value/src/resilient_kv.rs new file mode 100644 index 00000000..f8567f5b --- /dev/null +++ b/bd-key-value/src/resilient_kv.rs @@ -0,0 +1,105 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#![deny( + clippy::expect_used, + clippy::panic, + clippy::todo, + clippy::unimplemented, + clippy::unreachable, + clippy::unwrap_used +)] + +use crate::Storage; +use bd_bonjson::Value; +use bd_resilient_kv::KVStore; +use parking_lot::Mutex; +use std::path::Path; +use std::sync::Arc; + +#[cfg(test)] +#[path = "resilient_kv_test.rs"] +mod tests; + +/// A thread-safe wrapper around `KVStore` that implements the `Storage` trait. +/// +/// This container handles the mutability and concurrency concerns that arise from +/// the mismatch between `KVStore`'s mutable operations and the `Storage` trait's +/// immutable interface. It provides thread-safe access to the underlying `KVStore` +/// through interior mutability via a `Mutex`. +pub struct ResilientKvStorage { + store: Arc>, +} + +impl ResilientKvStorage { + /// Create a new `ResilientKvStorage` with the specified configuration. + /// + /// This is a convenience wrapper around `KVStore::new` that automatically + /// provides the necessary thread-safety wrapper. + /// + /// # Arguments + /// * `base_path` - Base path for the journal files (extensions will be added automatically) + /// * `buffer_size` - Size in bytes for each journal buffer + /// * `high_water_mark_ratio` - Optional ratio (0.0 to 1.0) for high water mark. Default: 0.8 + /// * `callback` - Optional callback function called when high water mark is exceeded + /// + /// # Errors + /// Returns an error if the underlying `KVStore` cannot be created/opened. + pub fn new>( + base_path: P, + buffer_size: usize, + high_water_mark_ratio: Option, + callback: Option, + ) -> anyhow::Result { + let kv_store = KVStore::new(base_path, buffer_size, high_water_mark_ratio, callback)?; + Ok(Self { + store: Arc::new(Mutex::new(kv_store)), + }) + } + + fn with_store(&self, f: F) -> anyhow::Result + where + F: FnOnce(&KVStore) -> anyhow::Result, + { + let store = self.store.lock(); + f(&store) + } + + fn with_store_mut(&self, f: F) -> anyhow::Result + where + F: FnOnce(&mut KVStore) -> anyhow::Result, + { + let mut store = self.store.lock(); + f(&mut store) + } +} + +impl Storage for ResilientKvStorage { + fn set_string(&self, key: &str, value: &str) -> anyhow::Result<()> { + self.with_store_mut(|store| { + store.insert(key.to_string(), Value::String(value.to_string()))?; + Ok(()) + }) + } + + fn get_string(&self, key: &str) -> anyhow::Result> { + self.with_store(|store| match store.get(key) { + Some(value) => match value { + Value::String(s) => Ok(Some(s.clone())), + _ => anyhow::bail!("Value at key '{key}' is not a string"), + }, + None => Ok(None), + }) + } + + fn delete(&self, key: &str) -> anyhow::Result<()> { + self.with_store_mut(|store| { + store.remove(key)?; + Ok(()) + }) + } +} diff --git a/bd-key-value/src/resilient_kv_test.rs b/bd-key-value/src/resilient_kv_test.rs new file mode 100644 index 00000000..b749eaaa --- /dev/null +++ b/bd-key-value/src/resilient_kv_test.rs @@ -0,0 +1,83 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use super::*; +use crate::{Storage, Store}; + +#[test] +fn test_resilient_kv_storage_with_storage_trait() { + // Create a temporary directory for the test + let temp_dir = std::env::temp_dir(); + let test_path = temp_dir.join("test_resilient_kv_storage"); + + // Clean up any existing files + let _ = std::fs::remove_file(test_path.with_extension("jrna")); + let _ = std::fs::remove_file(test_path.with_extension("jrnb")); + + // Create ResilientKvStorage + let storage = ResilientKvStorage::new(&test_path, 1024 * 1024, None, None).unwrap(); + + // Create Store with our ResilientKvStorage implementation + let store = Store::new(Box::new(storage)); + + // Test basic key-value operations through the Store interface + let test_key = crate::Key::new("test_string_key"); + let test_value = "Hello, World!".to_string(); + + // Set a value + store.set(&test_key, &test_value); + + // Get the value back + let retrieved_value = store.get(&test_key); + assert_eq!(retrieved_value, Some(test_value)); + + // Test with a key that doesn't exist + let non_existent_key: crate::Key = crate::Key::new("non_existent"); + let non_existent_value = store.get(&non_existent_key); + assert_eq!(non_existent_value, None); + + // Clean up + drop(store); + let _ = std::fs::remove_file(test_path.with_extension("jrna")); + let _ = std::fs::remove_file(test_path.with_extension("jrnb")); +} + +#[test] +fn test_resilient_kv_storage_direct_operations() { + // Create a temporary directory for the test + let temp_dir = std::env::temp_dir(); + let test_path = temp_dir.join("test_resilient_kv_direct"); + + // Clean up any existing files + let _ = std::fs::remove_file(test_path.with_extension("jrna")); + let _ = std::fs::remove_file(test_path.with_extension("jrnb")); + + // Create ResilientKvStorage + let storage = ResilientKvStorage::new(&test_path, 1024 * 1024, None, None).unwrap(); + + // Test direct Storage trait methods + let key = "direct_test_key"; + let value = "direct_test_value"; + + // Set string + storage.set_string(key, value).unwrap(); + + // Get string + let retrieved = storage.get_string(key).unwrap(); + assert_eq!(retrieved, Some(value.to_string())); + + // Delete + storage.delete(key).unwrap(); + + // Verify deletion + let after_delete = storage.get_string(key).unwrap(); + assert_eq!(after_delete, None); + + // Clean up + let _ = std::fs::remove_file(test_path.with_extension("jrna")); + let _ = std::fs::remove_file(test_path.with_extension("jrnb")); +}