Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bd-key-value/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bd-key-value/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,6 @@ where
self.key
}
}

pub mod resilient_kv;
pub use resilient_kv::ResilientKvStorage;
105 changes: 105 additions & 0 deletions bd-key-value/src/resilient_kv.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<KVStore>>,
}

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<P: AsRef<Path>>(
base_path: P,
buffer_size: usize,
high_water_mark_ratio: Option<f32>,
callback: Option<bd_resilient_kv::HighWaterMarkCallback>,
) -> anyhow::Result<Self> {
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<F, R>(&self, f: F) -> anyhow::Result<R>
where
F: FnOnce(&KVStore) -> anyhow::Result<R>,
{
let store = self.store.lock();
f(&store)
}

fn with_store_mut<F, R>(&self, f: F) -> anyhow::Result<R>
where
F: FnOnce(&mut KVStore) -> anyhow::Result<R>,
{
let mut store = self.store.lock();
f(&mut store)
}
}

impl Storage for ResilientKvStorage {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this later but now that there is a single implementation for Storage we should probably improve the Storage API a bit so that we can store more than just string types. For example the global fields are stored as a map serialized into a string but seems like we should be able to use better types within the storage at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the underlying system supports everything JSON does, and I've also developed a BONJSON extension to support other types (planned for inclusion into KSCrash, but we can also use it here), such as fixed-type arrays and dates and UUIDs. https://github.com/kstenerud/Orb

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<Option<String>> {
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(())
})
}
}
83 changes: 83 additions & 0 deletions bd-key-value/src/resilient_kv_test.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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"));
}
Loading