diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..78ee53d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Datacake is a batteries-included framework for building fault-tolerant distributed data systems in Rust. It implements eventually consistent replication using CRDTs (Conflict-free Replicated Data Types) with hybrid logical clocks, designed as an alternative to Raft consensus for specific use cases. + +## Commands + +### Building and Testing +- `cargo build` - Build the entire workspace +- `cargo test --workspace --exclude simulation-tests --features test-utils` - Run main test suite +- `cargo test --doc --workspace --exclude simulation-tests --features test-utils` - Run documentation tests +- `cargo nextest run --workspace --exclude simulation-tests --retries 3 --features test-utils` - Run tests with nextest (preferred) +- `cargo nextest run -p simulation-tests --retries 3` - Run simulation tests separately + +### Code Quality +- `cargo fmt --all -- --check` - Check code formatting (use nightly toolchain) +- `cargo clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo` - Run clippy lints +- `cargo hack check --all --ignore-private --each-feature --no-dev-deps` - Check feature combinations + +### Development Requirements +- Protobuf compiler (`protoc`) is required for building +- `cargo-hack` tool needed for feature checking: `cargo install cargo-hack` +- `cargo-nextest` tool recommended for testing: `cargo install cargo-nextest` + +## Architecture + +### Workspace Structure +Datacake is organized as a Cargo workspace with the following key crates: + +- **`datacake-crdt`** - Core CRDT implementation with HLCTimestamp (Hybrid Logical Clock) +- **`datacake-node`** - Cluster membership system built on chitchat (scuttlebutt gossip protocol) +- **`datacake-rpc`** - Zero-copy RPC framework using HTTP/2 and rkyv serialization +- **`datacake-eventual-consistency`** - High-level eventually consistent store framework +- **`datacake-sqlite`** - Pre-built SQLite storage implementation +- **`datacake-lmdb`** - Pre-built LMDB storage implementation + +### Core Concepts + +**Node Architecture**: Datacake uses a node-based architecture where each node runs a membership system, RPC server, and can have multiple extensions attached at runtime. + +**Extensions System**: The framework follows an extension pattern where storage backends and replication logic are implemented as `ClusterExtension` traits that can be added to nodes dynamically. + +**CRDT-Based Replication**: State synchronization uses ORSwot (Observed-Remove Set without Tombstones) CRDTs with hybrid logical clocks for causality tracking. + +**Storage Abstraction**: The `Storage` trait defines the interface for persistent backends, with implementations for SQLite and LMDB provided. + +### Key Files + +- `src/lib.rs` - Main crate re-exports with feature gates +- `datacake-eventual-consistency/src/storage.rs` - Core storage trait definition +- `datacake-node/src/lib.rs` - Node membership and extension system +- `datacake-crdt/src/orswot.rs` - CRDT implementation +- `datacake-rpc/src/` - RPC framework implementation + +## Development Guidelines + +### Features and Optional Dependencies +The project uses extensive feature flags. Default features include `datacake-crdt`, `datacake-rpc`, `datacake-eventual-consistency`, and `datacake-node`. Storage backends (SQLite/LMDB) and rkyv serialization support are optional. + +### Testing Strategy +- Unit tests for individual components +- Integration tests in `datacake-eventual-consistency/tests/` +- Simulation tests in `simulation-tests/` crate for distributed scenarios +- Test utilities provided in `test-helper/` crate + +### Code Style +- Uses rustfmt with custom configuration in `rustfmt.toml` +- Max width: 89 characters +- Nightly rustfmt features enabled +- Clippy with strict linting including `-Dclippy::todo` + +## Examples + +Check the `examples/` directory for practical implementations: +- `examples/replicated-kv/` - A distributed key-value store built on SQLite \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..9261fa4 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,353 @@ +# Migration Guide: Typed Keyspaces + +This guide helps you migrate from the previous version of Datacake (with hardcoded `u64` keys) to the new typed keyspaces system. + +## Overview of Changes + +Datacake now supports **per-keyspace typed keys**, allowing each keyspace to use semantically meaningful key types like `String`, `u64`, `Uuid`, or composite types. This is a **breaking change** that affects the public API. + +### What Changed + +- **Key Type**: Changed from `type Key = u64` to `type Key = Vec` +- **Storage Trait**: All methods now use `Vec` or `&[u8]` for keys +- **New API**: Added `TypedKeyspaceHandle` for type-safe keyspace access +- **Storage Backends**: LMDB and SQLite schemas updated to use byte-based keys + +### Backward Compatibility + +The good news: The underlying serialization format remains compatible. Since `Key` is now `Vec`, existing `u64` keys can be converted using `.to_le_bytes().to_vec()`. + +## Migration Strategies + +You have two options for migration: + +### Option 1: Minimal Changes (Quick Fix) + +Keep using `u64` keys with a helper function. This requires minimal code changes. + +**Before:** +```rust +let handle = store.handle(); + +handle.put( + "my-keyspace", + 1, // u64 key + b"data".to_vec(), + Consistency::All, +).await?; + +let doc = handle.get("my-keyspace", 1).await?; +``` + +**After:** +```rust +let handle = store.handle(); + +// Add a helper function +let key = |n: u64| n.to_le_bytes().to_vec(); + +handle.put( + "my-keyspace", + key(1), // Convert u64 to Vec + b"data".to_vec(), + Consistency::All, +).await?; + +let doc = handle.get("my-keyspace", key(1)).await?; +``` + +### Option 2: Adopt Typed API (Recommended) + +Use the new `TypedKeyspaceHandle` for better ergonomics and type safety. + +**Before:** +```rust +let handle = store.handle(); + +handle.put( + "users", + 1, + b"Alice".to_vec(), + Consistency::All, +).await?; +``` + +**After:** +```rust +// Option A: Continue using u64 with typed API +let users = store.typed_handle::("users")?; +users.put(1, b"Alice".to_vec(), Consistency::All).await?; + +// Option B: Switch to semantic String keys (breaking change for your app) +let users = store.typed_handle::("users")?; +users.put("user_1".to_string(), b"Alice".to_vec(), Consistency::All).await?; +``` + +## API Changes by Component + +### ReplicatedStoreHandle + +**Old API:** +```rust +pub async fn get(&self, keyspace: &str, doc_id: u64) -> Result, S::Error> +pub async fn put(&self, keyspace: &str, doc_id: u64, data: Vec, ...) -> Result<(), StoreError> +pub async fn del(&self, keyspace: &str, doc_id: u64, ...) -> Result<(), StoreError> +``` + +**New API:** +```rust +pub async fn get(&self, keyspace: &str, doc_id: Vec) -> Result, S::Error> +pub async fn put(&self, keyspace: &str, doc_id: Vec, data: Vec, ...) -> Result<(), StoreError> +pub async fn del(&self, keyspace: &str, doc_id: Vec, ...) -> Result<(), StoreError> + +// New typed API +pub fn typed_keyspace(&self, keyspace: impl Into) -> Result, TypeMismatchError> +``` + +### ReplicatedKeyspaceHandle + +**Old API:** +```rust +pub async fn get(&self, doc_id: u64) -> Result, S::Error> +pub async fn put(&self, doc_id: u64, data: Vec, ...) -> Result<(), StoreError> +``` + +**New API:** +```rust +pub async fn get(&self, doc_id: Vec) -> Result, S::Error> +pub async fn put(&self, doc_id: Vec, data: Vec, ...) -> Result<(), StoreError> +``` + +### Storage Trait + +**Old API:** +```rust +async fn get(&self, keyspace: &str, doc_id: u64) -> Result, Self::Error>; +async fn multi_get(&self, keyspace: &str, doc_ids: impl Iterator + Send) -> ...; +type MetadataIter: Iterator; +``` + +**New API:** +```rust +async fn get(&self, keyspace: &str, doc_id: Vec) -> Result, Self::Error>; +async fn multi_get(&self, keyspace: &str, doc_ids: impl Iterator> + Send) -> ...; +type MetadataIter: Iterator, HLCTimestamp, bool)>; +``` + +## Storage Backend Migration + +### SQLite + +The SQLite backend automatically handles the schema migration: + +- Column type changed from `INTEGER` to `BLOB` for `doc_id` +- Existing databases need schema migration (run migrations on startup) + +### LMDB + +The LMDB backend now uses `ByteSlice` instead of `U64`: + +- Database type changed from `Database, ByteSlice>` to `Database` +- Existing databases may need recreation or manual migration + +### Custom Storage Implementations + +If you have custom `Storage` trait implementations: + +1. Update all method signatures to use `Vec` or `&[u8]` for keys +2. Update `MetadataIter` type to yield `(Vec, HLCTimestamp, bool)` +3. Update internal storage to use byte keys +4. Run the storage test suite to verify correctness: + ```rust + datacake_eventual_consistency::storage::test_suite::run_test_suite(your_storage).await; + ``` + +## Using the Typed API + +### Creating Typed Handles + +```rust +use datacake::eventual_consistency::TypedKeyspaceHandle; +use datacake::crdt::DatacakeKey; + +// From EventuallyConsistentStore +let users = store.typed_handle::("users")?; + +// From ReplicatedStoreHandle +let counters = handle.typed_keyspace::("counters")?; +``` + +### Type Safety + +The typed API enforces type consistency at runtime: + +```rust +// First access with String keys +let users = store.typed_handle::("users")?; +users.put("user_1".to_string(), b"Alice".to_vec(), Consistency::All).await?; + +// Later access with same type - OK +let users2 = store.typed_handle::("users")?; + +// Try to access with different type - ERROR! +let fail = store.typed_handle::("users")?; +// Returns: Err(TypeMismatchError { keyspace: "users", expected: "alloc::string::String", actual: "u64" }) +``` + +### Supported Key Types + +Out of the box: +- `u64` - Numeric keys +- `String` - Text-based keys +- `Vec` - Raw byte keys +- `(String, String)` - Composite keys +- `uuid::Uuid` - UUID keys (requires `uuid` feature) + +Custom types can implement the `DatacakeKey` trait: + +```rust +use datacake::crdt::DatacakeKey; + +#[derive(Clone)] +struct CustomKey { + prefix: String, + id: u64, +} + +impl DatacakeKey for CustomKey { + fn to_bytes(&self) -> Vec { + // Implement serialization + let mut bytes = Vec::new(); + bytes.extend_from_slice(self.prefix.as_bytes()); + bytes.push(0); // Separator + bytes.extend_from_slice(&self.id.to_le_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + // Implement deserialization + // ... + } +} +``` + +## Common Migration Patterns + +### Pattern 1: Batch Operations + +**Before:** +```rust +let keys = vec![1, 2, 3]; +let docs = handle.get_many("keyspace", keys).await?; +``` + +**After:** +```rust +// Option A: Helper function +let key = |n: u64| n.to_le_bytes().to_vec(); +let keys = vec![key(1), key(2), key(3)]; +let docs = handle.get_many("keyspace", keys).await?; + +// Option B: Typed API +let kspace = handle.typed_keyspace::("keyspace")?; +let docs = kspace.get_many(vec![1, 2, 3]).await?; +``` + +### Pattern 2: Iteration + +**Before:** +```rust +for (id, ts, is_tombstone) in storage.iter_metadata("keyspace").await? { + // id is u64 + println!("Document {}: {}", id, ts); +} +``` + +**After:** +```rust +// Helper to convert bytes back to u64 +fn bytes_to_u64(bytes: &[u8]) -> u64 { + u64::from_le_bytes(bytes.try_into().unwrap()) +} + +for (id, ts, is_tombstone) in storage.iter_metadata("keyspace").await? { + // id is Vec + let id_num = bytes_to_u64(&id); + println!("Document {}: {}", id_num, ts); +} +``` + +### Pattern 3: Tests + +**Before:** +```rust +#[tokio::test] +async fn test_storage() { + store.put("test", 1, b"data".to_vec(), Consistency::All).await.unwrap(); + let doc = store.get("test", 1).await.unwrap(); + assert!(doc.is_some()); +} +``` + +**After:** +```rust +// Add helper at module level +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + +#[tokio::test] +async fn test_storage() { + store.put("test", key(1), b"data".to_vec(), Consistency::All).await.unwrap(); + let doc = store.get("test", key(1)).await.unwrap(); + assert!(doc.is_some()); +} +``` + +## FAQ + +### Q: Will this break my existing data? + +A: No, existing data remains compatible. The serialization format for `u64` keys (little-endian bytes) is preserved when converted to `Vec`. + +### Q: Do I need to migrate my storage backend? + +A: SQLite and LMDB users should review the schema changes. Custom implementations need to update their `Storage` trait implementation. + +### Q: Can I use different key types in different keyspaces? + +A: Yes! That's the whole point. Each keyspace can have its own key type, enforced at runtime. + +### Q: What's the performance impact? + +A: Minimal. The typed API is a zero-cost abstraction at runtime. The underlying storage still uses efficient byte representations. + +### Q: Can I mix typed and untyped APIs? + +A: Yes, but be careful. The untyped API always uses `Vec`, while the typed API converts your key type to `Vec`. As long as the byte representation matches, they're compatible. + +### Q: How do I know which key type a keyspace uses? + +A: The system tracks this internally. If you try to access a keyspace with the wrong type, you'll get a `TypeMismatchError`. + +## Getting Help + +If you encounter issues during migration: + +1. Check the [examples](https://github.com/lnx-search/datacake/tree/main/examples) for reference implementations +2. Run the storage test suite to verify your custom implementations +3. Review the API documentation for `TypedKeyspaceHandle` +4. Open an issue on GitHub if you find bugs or need clarification + +## Summary + +**Quick Migration Checklist:** + +- [ ] Add helper function: `let key = |n: u64| n.to_le_bytes().to_vec();` +- [ ] Replace all numeric key arguments with `key(n)` +- [ ] Update custom `Storage` implementations to use `Vec` +- [ ] Consider migrating to typed API for better ergonomics +- [ ] Test thoroughly with your data +- [ ] Update your documentation + +Welcome to type-safe keyspaces in Datacake! diff --git a/README.md b/README.md index b443356..6ef594c 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,16 @@ async fn main() -> anyhow::Result<()> { .add_extension(EventuallyConsistentStoreExtension::new(MemStore::default())) .await .expect("Create store."); - + let handle = store.handle(); + // Helper to convert u64 to Vec key + let key = |n: u64| n.to_le_bytes().to_vec(); + handle .put( "my-keyspace", - 1, + key(1), b"Hello, world! From keyspace 1.".to_vec(), Consistency::All, ) @@ -75,6 +78,28 @@ async fn main() -> anyhow::Result<()> { } ``` +#### Type-Safe Keyspaces +Datacake supports type-safe keyspaces where each keyspace can use a different key type. This provides compile-time safety and better ergonomics: + +```rust +use datacake::eventual_consistency::TypedKeyspaceHandle; + +// Create type-safe handles for different keyspaces +let users: TypedKeyspaceHandle = store.typed_handle("users")?; +let counters: TypedKeyspaceHandle = store.typed_handle("counters")?; + +// Use String keys directly - no manual conversion needed! +users.put("user_123".to_string(), b"Alice".to_vec(), Consistency::All).await?; + +// Use u64 keys directly +counters.put(42, b"count_data".to_vec(), Consistency::All).await?; + +// Type safety: This would fail at runtime if you try to mix types +// let fail = store.typed_handle::("users")?; // Error: type mismatch! +``` + +Supported key types include `u64`, `String`, `Vec`, `uuid::Uuid` (with `uuid` feature), and composite types like `(String, String)`. + ### Why does Datacake exist? Datacake is the result of my attempts at bringing high-availability to [lnx](https://github.com/lnx-search/lnx) diff --git a/benchmarks/src/replication/datacake_memory.rs b/benchmarks/src/replication/datacake_memory.rs index a729c55..f10c04f 100644 --- a/benchmarks/src/replication/datacake_memory.rs +++ b/benchmarks/src/replication/datacake_memory.rs @@ -69,7 +69,7 @@ async fn insert_n_docs( handle .put( "my-keyspace", - id, + id.to_le_bytes().to_vec(), b"Hello, world! From keyspace 1.".to_vec(), consistency, ) @@ -84,7 +84,7 @@ async fn remove_n_docs( consistency: Consistency, ) -> Result<()> { for id in range { - handle.del("my-keyspace", id, consistency).await?; + handle.del("my-keyspace", id.to_le_bytes().to_vec(), consistency).await?; } Ok(()) } diff --git a/benchmarks/src/stores/memstore.rs b/benchmarks/src/stores/memstore.rs index 7513d64..46bdf62 100644 --- a/benchmarks/src/stores/memstore.rs +++ b/benchmarks/src/stores/memstore.rs @@ -41,7 +41,7 @@ impl Storage for MemStore { if let Some(ks) = self.metadata.read().get(keyspace) { return Ok(ks .iter() - .map(|(k, (ts, tombstone))| (*k, *ts, *tombstone)) + .map(|(k, (ts, tombstone))| (k.clone(), *ts, *tombstone)) .collect::>() .into_iter()); }; @@ -80,12 +80,12 @@ impl Storage for MemStore { .entry(keyspace.to_string()) .and_modify(|entries| { for doc in documents.clone() { - entries.insert(doc.id(), doc); + entries.insert(doc.id().to_vec(), doc); } }) .or_insert_with(|| { HashMap::from_iter( - documents.clone().into_iter().map(|doc| (doc.id(), doc)), + documents.clone().into_iter().map(|doc| (doc.id().to_vec(), doc)), ) }); self.metadata @@ -93,14 +93,14 @@ impl Storage for MemStore { .entry(keyspace.to_string()) .and_modify(|entries| { for doc in documents.clone() { - entries.insert(doc.id(), (doc.last_updated(), false)); + entries.insert(doc.id().to_vec(), (doc.last_updated(), false)); } }) .or_insert_with(|| { HashMap::from_iter( documents .into_iter() - .map(|doc| (doc.id(), (doc.last_updated(), false))), + .map(|doc| (doc.id().to_vec(), (doc.last_updated(), false))), ) }); diff --git a/datacake-crdt/Cargo.toml b/datacake-crdt/Cargo.toml index a251b7b..9074afb 100644 --- a/datacake-crdt/Cargo.toml +++ b/datacake-crdt/Cargo.toml @@ -13,10 +13,14 @@ readme = "README.md" [dependencies] thiserror = "1.0.33" +uuid = { version = "1.0", optional = true, features = ["v4"] } rkyv = { version = "0.7.42", features = ["strict", "archive_le"], optional = true } [features] # Enables (de)serialization support for all data types. rkyv-support = ["rkyv"] -rkyv-validation = ["rkyv-support", "rkyv/validation"] \ No newline at end of file +rkyv-validation = ["rkyv-support", "rkyv/validation"] + +# Enables UUID key support +uuid = ["dep:uuid"] \ No newline at end of file diff --git a/datacake-crdt/src/key.rs b/datacake-crdt/src/key.rs new file mode 100644 index 0000000..2be657f --- /dev/null +++ b/datacake-crdt/src/key.rs @@ -0,0 +1,283 @@ +//! Key type system for Datacake. +//! +//! This module defines the `DatacakeKey` trait and provides implementations +//! for common key types including u64, String, Vec, Uuid, and composite keys. + +use std::fmt::Debug; + +#[cfg(feature = "uuid")] +use uuid::Uuid; + +/// Maximum allowed size for serialized keys (4KB). +/// +/// This limit prevents abuse and ensures reasonable memory usage. +pub const MAX_KEY_SIZE: usize = 4096; + +/// Error type for key deserialization failures. +#[derive(Debug, thiserror::Error)] +pub enum KeyDeserializationError { + #[error("Key size {0} bytes exceeds maximum allowed size of {MAX_KEY_SIZE} bytes")] + KeyTooLarge(usize), + + #[error("Invalid key format: {0}")] + InvalidFormat(String), + + #[error("UTF-8 decoding error: {0}")] + Utf8Error(#[from] std::string::FromUtf8Error), + + #[cfg(feature = "uuid")] + #[error("UUID parsing error: {0}")] + UuidError(#[from] uuid::Error), +} + +/// Trait defining the interface for key types used in Datacake keyspaces. +/// +/// This trait allows each keyspace to use a semantically appropriate key type +/// (e.g., String for user IDs, Uuid for sessions, u64 for performance-critical counters). +/// +/// # Example +/// +/// ``` +/// use datacake_crdt::{DatacakeKey, KeyDeserializationError}; +/// +/// // String keys for user-facing identifiers +/// let user_key = "user_123".to_string(); +/// let bytes = user_key.to_bytes(); +/// let restored = String::from_bytes(&bytes).unwrap(); +/// assert_eq!(user_key, restored); +/// ``` +pub trait DatacakeKey: Clone + std::hash::Hash + Eq + Debug + Send + Sync + 'static { + /// Serialize the key to bytes for storage and transmission. + /// + /// The serialized form must not exceed `MAX_KEY_SIZE` bytes. + fn to_bytes(&self) -> Vec; + + /// Deserialize a key from bytes. + /// + /// # Errors + /// + /// Returns an error if: + /// - The byte slice is too large (exceeds `MAX_KEY_SIZE`) + /// - The byte slice cannot be deserialized to the target type + fn from_bytes(bytes: &[u8]) -> Result; +} + +// ======================================== +// Implementation for u64 (current default) +// ======================================== + +impl DatacakeKey for u64 { + fn to_bytes(&self) -> Vec { + self.to_le_bytes().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEY_SIZE { + return Err(KeyDeserializationError::KeyTooLarge(bytes.len())); + } + + if bytes.len() != 8 { + return Err(KeyDeserializationError::InvalidFormat( + format!("Expected 8 bytes for u64, got {}", bytes.len()) + )); + } + + let mut array = [0u8; 8]; + array.copy_from_slice(bytes); + Ok(u64::from_le_bytes(array)) + } +} + +// ======================================== +// Implementation for String +// ======================================== + +impl DatacakeKey for String { + fn to_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEY_SIZE { + return Err(KeyDeserializationError::KeyTooLarge(bytes.len())); + } + + String::from_utf8(bytes.to_vec()).map_err(Into::into) + } +} + +// ======================================== +// Implementation for Vec +// ======================================== + +impl DatacakeKey for Vec { + fn to_bytes(&self) -> Vec { + self.clone() + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEY_SIZE { + return Err(KeyDeserializationError::KeyTooLarge(bytes.len())); + } + + Ok(bytes.to_vec()) + } +} + +// ======================================== +// Implementation for Uuid +// ======================================== + +#[cfg(feature = "uuid")] +impl DatacakeKey for Uuid { + fn to_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEY_SIZE { + return Err(KeyDeserializationError::KeyTooLarge(bytes.len())); + } + + if bytes.len() != 16 { + return Err(KeyDeserializationError::InvalidFormat( + format!("Expected 16 bytes for UUID, got {}", bytes.len()) + )); + } + + Uuid::from_slice(bytes).map_err(Into::into) + } +} + +// ======================================== +// Implementation for composite key (String, String) +// ======================================== + +impl DatacakeKey for (String, String) { + fn to_bytes(&self) -> Vec { + // Format: [len1: u32 | data1 | len2: u32 | data2] + let bytes1 = self.0.as_bytes(); + let bytes2 = self.1.as_bytes(); + + let len1 = bytes1.len() as u32; + let len2 = bytes2.len() as u32; + + let mut result = Vec::with_capacity(8 + bytes1.len() + bytes2.len()); + result.extend_from_slice(&len1.to_le_bytes()); + result.extend_from_slice(bytes1); + result.extend_from_slice(&len2.to_le_bytes()); + result.extend_from_slice(bytes2); + + result + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEY_SIZE { + return Err(KeyDeserializationError::KeyTooLarge(bytes.len())); + } + + if bytes.len() < 8 { + return Err(KeyDeserializationError::InvalidFormat( + "Composite key too short".to_string() + )); + } + + // Read first length + let mut len1_bytes = [0u8; 4]; + len1_bytes.copy_from_slice(&bytes[0..4]); + let len1 = u32::from_le_bytes(len1_bytes) as usize; + + if bytes.len() < 8 + len1 { + return Err(KeyDeserializationError::InvalidFormat( + "Invalid first component length".to_string() + )); + } + + // Read first string + let str1 = String::from_utf8(bytes[4..4 + len1].to_vec())?; + + // Read second length + let mut len2_bytes = [0u8; 4]; + len2_bytes.copy_from_slice(&bytes[4 + len1..8 + len1]); + let len2 = u32::from_le_bytes(len2_bytes) as usize; + + if bytes.len() != 8 + len1 + len2 { + return Err(KeyDeserializationError::InvalidFormat( + "Invalid second component length".to_string() + )); + } + + // Read second string + let str2 = String::from_utf8(bytes[8 + len1..].to_vec())?; + + Ok((str1, str2)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u64_key_roundtrip() { + let key = 42u64; + let bytes = key.to_bytes(); + let restored = u64::from_bytes(&bytes).unwrap(); + assert_eq!(key, restored); + } + + #[test] + fn test_string_key_roundtrip() { + let key = "user_123".to_string(); + let bytes = key.to_bytes(); + let restored = String::from_bytes(&bytes).unwrap(); + assert_eq!(key, restored); + } + + #[test] + fn test_vec_key_roundtrip() { + let key = vec![1u8, 2, 3, 4, 5]; + let bytes = key.to_bytes(); + let restored = Vec::::from_bytes(&bytes).unwrap(); + assert_eq!(key, restored); + } + + #[cfg(feature = "uuid")] + #[test] + fn test_uuid_key_roundtrip() { + let key = Uuid::new_v4(); + let bytes = DatacakeKey::to_bytes(&key); + let restored = ::from_bytes(&bytes).unwrap(); + assert_eq!(key, restored); + } + + #[test] + fn test_composite_key_roundtrip() { + let key = ("tenant_123".to_string(), "resource_456".to_string()); + let bytes = key.to_bytes(); + let restored = <(String, String)>::from_bytes(&bytes).unwrap(); + assert_eq!(key, restored); + } + + #[test] + fn test_key_size_limit() { + let large_key = "x".repeat(MAX_KEY_SIZE + 1); + let bytes = large_key.as_bytes(); + let result = String::from_bytes(bytes); + assert!(matches!(result, Err(KeyDeserializationError::KeyTooLarge(_)))); + } + + #[test] + fn test_invalid_u64_size() { + let bytes = vec![1u8, 2, 3]; // Wrong size + let result = u64::from_bytes(&bytes); + assert!(matches!(result, Err(KeyDeserializationError::InvalidFormat(_)))); + } + + #[test] + fn test_invalid_utf8() { + let invalid_utf8 = vec![0xFF, 0xFE, 0xFD]; + let result = String::from_bytes(&invalid_utf8); + assert!(matches!(result, Err(KeyDeserializationError::Utf8Error(_)))); + } +} diff --git a/datacake-crdt/src/lib.rs b/datacake-crdt/src/lib.rs index ab656fd..e3a7d92 100644 --- a/datacake-crdt/src/lib.rs +++ b/datacake-crdt/src/lib.rs @@ -21,16 +21,16 @@ //! let mut node_b_set = OrSWotSet::<1>::default(); //! //! // Insert a new key with a new timestamp in set A. -//! node_a_set.insert(1, node_a.send().unwrap()); +//! node_a_set.insert(vec![1], node_a.send().unwrap()); //! //! // Insert a new entry in set B. -//! node_b_set.insert(2, node_b.send().unwrap()); +//! node_b_set.insert(vec![2], node_b.send().unwrap()); //! //! // Let some time pass for demonstration purposes. //! std::thread::sleep(Duration::from_millis(500)); //! //! // Set A has key `1` removed. -//! node_a_set.delete(1, node_a.send().unwrap()); +//! node_a_set.delete(vec![1], node_a.send().unwrap()); //! //! // Merging set B with set A and vice versa. //! // Our sets are now aligned without conflicts. @@ -38,8 +38,8 @@ //! node_a_set.merge(node_b_set.clone()); //! //! // Set A and B should both see that key `1` has been deleted. -//! assert!(node_a_set.get(&1).is_none(), "Key should be correctly removed."); -//! assert!(node_b_set.get(&1).is_none(), "Key should be correctly removed."); +//! assert!(node_a_set.get(&vec![1]).is_none(), "Key should be correctly removed."); +//! assert!(node_b_set.get(&vec![1]).is_none(), "Key should be correctly removed."); //! ``` //! //! ### Inspirations @@ -47,9 +47,11 @@ //! - [Big(ger) Sets: Making CRDT Sets Scale in Riak by Russell Brown](https://www.youtube.com/watch?v=f20882ZSdkU) //! - ["CRDTs Illustrated" by Arnout Engelen](https://www.youtube.com/watch?v=9xFfOhasiOE) +mod key; mod orswot; mod timestamp; +pub use key::{DatacakeKey, KeyDeserializationError, MAX_KEY_SIZE}; #[cfg(feature = "rkyv-support")] pub use orswot::BadState; pub use orswot::{Key, OrSWotSet, StateChanges}; diff --git a/datacake-crdt/src/orswot.rs b/datacake-crdt/src/orswot.rs index ec90840..2abb0a7 100644 --- a/datacake-crdt/src/orswot.rs +++ b/datacake-crdt/src/orswot.rs @@ -9,7 +9,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::timestamp::HLCTimestamp; -pub type Key = u64; +pub type Key = Vec; pub type StateChanges = Vec<(Key, HLCTimestamp)>; /// The period of time, to remove from the @@ -199,13 +199,13 @@ impl NodeVersions { /// let mut node_b_set = OrSWotSet::<1>::default(); /// /// // Insert a new key with a new timestamp in set A. -/// node_a_set.insert(1, node_a.send().unwrap()); +/// node_a_set.insert(vec![1], node_a.send().unwrap()); /// /// // Insert a new entry in set B. -/// node_b_set.insert(2, node_b.send().unwrap()); +/// node_b_set.insert(vec![2], node_b.send().unwrap()); /// /// // Set A has key `1` removed. -/// node_a_set.delete(1, node_a.send().unwrap()); +/// node_a_set.delete(vec![1], node_a.send().unwrap()); /// /// // Merging set B with set A and vice versa. /// // Our sets are now aligned without conflicts. @@ -213,8 +213,8 @@ impl NodeVersions { /// node_a_set.merge(node_b_set.clone()); /// /// // Set A and B should both see that key `1` has been deleted. -/// assert!(node_a_set.get(&1).is_none(), "Key should be correctly removed."); -/// assert!(node_b_set.get(&1).is_none(), "Key should be correctly removed."); +/// assert!(node_a_set.get(&vec![1]).is_none(), "Key should be correctly removed."); +/// assert!(node_b_set.get(&vec![1]).is_none(), "Key should be correctly removed."); /// ``` pub struct OrSWotSet { entries: BTreeMap, @@ -251,11 +251,11 @@ impl OrSWotSet { let mut removals = Vec::new(); for (key, ts) in other.entries.iter() { - self.check_self_then_insert_to(*key, *ts, &mut changes); + self.check_self_then_insert_to(key.clone(), *ts, &mut changes); } for (key, ts) in other.dead.iter() { - self.check_self_then_insert_to(*key, *ts, &mut removals); + self.check_self_then_insert_to(key.clone(), *ts, &mut removals); } (changes, removals) @@ -538,6 +538,11 @@ mod tests { use super::*; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + #[test] fn test_op_order() { let mut node_a = HLCTimestamp::now(0, 0); @@ -548,29 +553,29 @@ mod tests { let ts_a = node_a.send().unwrap(); let ts_b = node_b.send().unwrap(); - node_a_set.insert(1, ts_a); - node_a_set.insert(1, ts_b); + node_a_set.insert(key(1), ts_a); + node_a_set.insert(key(1), ts_b); - let retrieved = node_a_set.get(&1); + let retrieved = node_a_set.get(&key(1)); assert_eq!(retrieved, Some(&ts_b), "Node B should win the operation."); let mut node_a_set = OrSWotSet::<1>::default(); let mut node_b_set = OrSWotSet::<1>::default(); - node_a_set.insert(1, ts_a); - node_b_set.insert(1, ts_b); + node_a_set.insert(key(1), ts_a); + node_b_set.insert(key(1), ts_b); node_a_set.merge(node_b_set.clone()); node_b_set.merge(node_a_set.clone()); - let retrieved = node_a_set.get(&1); + let retrieved = node_a_set.get(&key(1)); assert_eq!( retrieved, Some(&ts_b), "Node B should win the operation after merging set A." ); - let retrieved = node_b_set.get(&1); + let retrieved = node_b_set.get(&key(1)); assert_eq!( retrieved, Some(&ts_b), @@ -587,16 +592,16 @@ mod tests { let mut node_a_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); - node_a_set.insert(3, node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); + node_a_set.insert(key(3), node_a.send().unwrap()); // We create our new state on node b's side. let mut node_b_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_b_set.insert(1, node_b.send().unwrap()); - node_b_set.insert(4, node_b.send().unwrap()); + node_b_set.insert(key(1), node_b.send().unwrap()); + node_b_set.insert(key(4), node_b.send().unwrap()); node_a_set.merge(node_b_set); @@ -606,19 +611,19 @@ mod tests { ); assert!( - node_a_set.entries.get(&1).is_some(), + node_a_set.entries.get(&key(1)).is_some(), "Expected entry with key 1 to exist." ); assert!( - node_a_set.entries.get(&2).is_some(), + node_a_set.entries.get(&key(2)).is_some(), "Expected entry with key 2 to exist." ); assert!( - node_a_set.entries.get(&3).is_some(), + node_a_set.entries.get(&key(3)).is_some(), "Expected entry with key 3 to exist." ); assert!( - node_a_set.entries.get(&4).is_some(), + node_a_set.entries.get(&key(4)).is_some(), "Expected entry with key 4 to exist." ); } @@ -634,9 +639,9 @@ mod tests { // We add a new set of entries into our set. // It's important that our `3` key is first here, as it means the counter // of the HLC timestamp will mean the delete succeeds. - node_a_set.insert(3, node_a.send().unwrap()); - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); + node_a_set.insert(key(3), node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); // We create our new state on node b's side. let mut node_b_set = OrSWotSet::<1>::default(); @@ -644,28 +649,28 @@ mod tests { // We add a new set of entries into our set. // These entries effectively happen at the same time as node A in our test, just because // of the execution speed. - node_b_set.insert(1, node_b.send().unwrap()); - node_b_set.delete(3, node_b.send().unwrap()); + node_b_set.insert(key(1), node_b.send().unwrap()); + node_b_set.delete(key(3), node_b.send().unwrap()); // When merged, the set should mark key `3` as deleted // and ignore the insert on the original set. node_a_set.merge(node_b_set.clone()); assert!( - node_a_set.dead.contains_key(&3), + node_a_set.dead.contains_key(&key(3)), "SET A: Expected key 3 to be marked as dead." ); assert!( - node_a_set.entries.get(&1).is_some(), + node_a_set.entries.get(&key(1)).is_some(), "SET A: Expected entry with key 1 to exist." ); assert!( - node_a_set.entries.get(&2).is_some(), + node_a_set.entries.get(&key(2)).is_some(), "SET A: Expected entry with key 2 to exist." ); assert!( - node_a_set.entries.get(&3).is_none(), + node_a_set.entries.get(&key(3)).is_none(), "SET A: Expected entry with key 3 to NOT exist." ); @@ -673,20 +678,20 @@ mod tests { node_b_set.merge(node_a_set); assert!( - node_b_set.dead.contains_key(&3), + node_b_set.dead.contains_key(&key(3)), "SET B: Expected key 3 to be marked as dead." ); assert!( - node_b_set.entries.get(&1).is_some(), + node_b_set.entries.get(&key(1)).is_some(), "SET B: Expected entry with key 1 to exist." ); assert!( - node_b_set.entries.get(&2).is_some(), + node_b_set.entries.get(&key(2)).is_some(), "SET B: Expected entry with key 2 to exist." ); assert!( - node_b_set.entries.get(&3).is_none(), + node_b_set.entries.get(&key(3)).is_none(), "SET B: Expected entry with key 3 to NOT exist." ); } @@ -704,34 +709,34 @@ mod tests { let mut node_a_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); - node_a_set.insert(3, node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); + node_a_set.insert(key(3), node_a.send().unwrap()); // We create our new state on node b's side. let mut node_b_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_b_set.insert(1, node_b.send().unwrap()); - node_b_set.delete(3, node_b.send().unwrap()); + node_b_set.insert(key(1), node_b.send().unwrap()); + node_b_set.delete(key(3), node_b.send().unwrap()); node_a_set.merge(node_b_set.clone()); assert!( - node_a_set.dead.contains_key(&3), + node_a_set.dead.contains_key(&key(3)), "Expected key 3 to be marked as dead." ); assert!( - node_a_set.entries.get(&1).is_some(), + node_a_set.entries.get(&key(1)).is_some(), "Expected entry with key 1 to exist." ); assert!( - node_a_set.entries.get(&2).is_some(), + node_a_set.entries.get(&key(2)).is_some(), "Expected entry with key 2 to exist." ); assert!( - node_a_set.entries.get(&3).is_none(), + node_a_set.entries.get(&key(3)).is_none(), "Expected entry with key 3 to NOT exist." ); } @@ -749,23 +754,23 @@ mod tests { let mut node_a_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); - node_a_set.insert(3, node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); + node_a_set.insert(key(3), node_a.send().unwrap()); // We create our new state on node b's side. let mut node_b_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_b_set.insert(1, node_b.send().unwrap()); - node_b_set.delete(3, node_b.send().unwrap()); + node_b_set.insert(key(1), node_b.send().unwrap()); + node_b_set.delete(key(3), node_b.send().unwrap()); node_a_set.merge(node_b_set.clone()); - node_a_set.insert(4, node_a.send().unwrap()); + node_a_set.insert(key(4), node_a.send().unwrap()); // We must observe another event from node b. - node_b_set.insert(4, node_b.send().unwrap()); + node_b_set.insert(key(4), node_b.send().unwrap()); node_a_set.merge(node_b_set.clone()); node_a_set.purge_old_deletes(); @@ -776,19 +781,19 @@ mod tests { ); assert!( - node_a_set.entries.get(&1).is_some(), + node_a_set.entries.get(&key(1)).is_some(), "Expected entry with key 1 to exist." ); assert!( - node_a_set.entries.get(&2).is_some(), + node_a_set.entries.get(&key(2)).is_some(), "Expected entry with key 2 to exist." ); assert!( - node_a_set.entries.get(&3).is_none(), + node_a_set.entries.get(&key(3)).is_none(), "Expected entry with key 3 to NOT exist." ); assert!( - node_a_set.entries.get(&4).is_some(), + node_a_set.entries.get(&key(4)).is_some(), "Expected entry with key 4 to exist." ); } @@ -806,9 +811,9 @@ mod tests { let mut node_a_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); - node_a_set.insert(3, node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); + node_a_set.insert(key(3), node_a.send().unwrap()); std::thread::sleep(Duration::from_millis(1)); @@ -816,18 +821,18 @@ mod tests { let mut node_b_set = OrSWotSet::<1>::default(); // We add a new set of entries into our set. - node_b_set.insert(1, node_b.send().unwrap()); - node_b_set.delete(3, node_b.send().unwrap()); + node_b_set.insert(key(1), node_b.send().unwrap()); + node_b_set.delete(key(3), node_b.send().unwrap()); node_a_set.merge(node_b_set.clone()); - node_a_set.insert(4, node_a.send().unwrap()); + node_a_set.insert(key(4), node_a.send().unwrap()); // Delete entry 2 from set a. - node_a_set.delete(2, node_a.send().unwrap()); + node_a_set.delete(key(2), node_a.send().unwrap()); // 'observe' a new op happening from node a. - node_a_set.insert(5, node_a.send().unwrap()); + node_a_set.insert(key(5), node_a.send().unwrap()); node_a_set.merge(node_b_set.clone()); @@ -838,54 +843,54 @@ mod tests { node_a_set.purge_old_deletes(); assert!( - node_a_set.dead.get(&3).is_some(), + node_a_set.dead.get(&key(3)).is_some(), "SET A: Expected key 3 to be left in dead set." ); assert!( - node_a_set.dead.get(&2).is_none(), + node_a_set.dead.get(&key(2)).is_none(), "SET A: Expected key 2 to be purged from dead set." ); assert!( - node_a_set.entries.get(&1).is_some(), + node_a_set.entries.get(&key(1)).is_some(), "SET A: Expected entry with key 1 to exist." ); assert!( - node_a_set.entries.get(&2).is_none(), + node_a_set.entries.get(&key(2)).is_none(), "SET A: Expected entry with key 2 to exist." ); assert!( - node_a_set.entries.get(&3).is_none(), + node_a_set.entries.get(&key(3)).is_none(), "SET A: Expected entry with key 3 to NOT exist." ); assert!( - node_a_set.entries.get(&4).is_some(), + node_a_set.entries.get(&key(4)).is_some(), "SET A: Expected entry with key 4 to exist." ); assert!( - node_b_set.dead.get(&3).is_some(), + node_b_set.dead.get(&key(3)).is_some(), "SET B: Expected key 3 to be left in dead set." ); assert!( - node_b_set.dead.get(&2).is_none(), + node_b_set.dead.get(&key(2)).is_none(), "SET B: Expected key 2 to be purged from dead set." ); assert!( - node_b_set.entries.get(&1).is_some(), + node_b_set.entries.get(&key(1)).is_some(), "SET B: Expected entry with key 1 to exist." ); assert!( - node_b_set.entries.get(&2).is_none(), + node_b_set.entries.get(&key(2)).is_none(), "SET B: Expected entry with key 2 to exist." ); assert!( - node_b_set.entries.get(&3).is_none(), + node_b_set.entries.get(&key(3)).is_none(), "SET B: Expected entry with key 3 to NOT exist." ); assert!( - node_b_set.entries.get(&4).is_some(), + node_b_set.entries.get(&key(4)).is_some(), "SET B: Expected entry with key 4 to exist." ); } @@ -898,10 +903,10 @@ mod tests { // We create our new set for node a. let mut node_a_set = OrSWotSet::<1>::default(); - let did_add = node_a_set.insert(1, node_a.send().unwrap()); + let did_add = node_a_set.insert(key(1), node_a.send().unwrap()); assert!(did_add, "Expected entry insert to be added."); - let did_add = node_a_set.insert(1, old_ts); + let did_add = node_a_set.insert(key(1), old_ts); assert!( !did_add, "Expected entry insert with old timestamp to be ignored" @@ -916,10 +921,10 @@ mod tests { // We create our new set for node a. let mut node_a_set = OrSWotSet::<1>::default(); - let did_add = node_a_set.insert(1, node_a.send().unwrap()); + let did_add = node_a_set.insert(key(1), node_a.send().unwrap()); assert!(did_add, "Expected entry insert to be added."); - let did_add = node_a_set.delete(1, old_ts); + let did_add = node_a_set.delete(key(1), old_ts); assert!( !did_add, "Expected entry delete with old timestamp to be ignored" @@ -939,12 +944,12 @@ mod tests { let mut node_b_set = OrSWotSet::<1>::default(); let insert_ts_1 = node_a.send().unwrap(); - node_a_set.insert(1, insert_ts_1); + node_a_set.insert(key(1), insert_ts_1); let (changed, removed) = OrSWotSet::<1>::default().diff(&node_a_set); assert_eq!( changed, - vec![(1, insert_ts_1)], + vec![(key(1), insert_ts_1)], "Expected set diff to contain key `1`." ); assert!( @@ -953,16 +958,16 @@ mod tests { ); let delete_ts_3 = node_a.send().unwrap(); - node_a_set.delete(3, delete_ts_3); + node_a_set.delete(key(3), delete_ts_3); let insert_ts_2 = node_b.send().unwrap(); - node_b_set.insert(2, insert_ts_2); + node_b_set.insert(key(2), insert_ts_2); let (changed, removed) = node_a_set.diff(&node_b_set); assert_eq!( changed, - vec![(2, insert_ts_2)], + vec![(key(2), insert_ts_2)], "Expected set a to only be marked as missing key `2`" ); assert!( @@ -973,12 +978,12 @@ mod tests { let (changed, removed) = node_b_set.diff(&node_a_set); assert_eq!( changed, - vec![(1, insert_ts_1)], + vec![(key(1), insert_ts_1)], "Expected set b to have key `1` marked as changed." ); assert_eq!( removed, - vec![(3, delete_ts_3)], + vec![(key(3), delete_ts_3)], "Expected set b to have key `3` marked as deleted." ); } @@ -996,25 +1001,25 @@ mod tests { let mut node_b_set = OrSWotSet::<1>::default(); // This should get overriden by node b. - node_a_set.insert(1, node_a.send().unwrap()); - node_a_set.insert(2, node_a.send().unwrap()); + node_a_set.insert(key(1), node_a.send().unwrap()); + node_a_set.insert(key(2), node_a.send().unwrap()); std::thread::sleep(Duration::from_millis(500)); let delete_ts_3 = node_a.send().unwrap(); - node_a_set.delete(3, delete_ts_3); + node_a_set.delete(key(3), delete_ts_3); let insert_ts_2 = node_b.send().unwrap(); - node_b_set.insert(2, insert_ts_2); + node_b_set.insert(key(2), insert_ts_2); let insert_ts_1 = node_b.send().unwrap(); - node_b_set.insert(1, insert_ts_1); + node_b_set.insert(key(1), insert_ts_1); let (changed, removed) = node_a_set.diff(&node_b_set); assert_eq!( changed, - vec![(1, insert_ts_1), (2, insert_ts_2)], + vec![(key(1), insert_ts_1), (key(2), insert_ts_2)], "Expected set a to be marked as updating keys `1, 2`" ); assert!( @@ -1030,7 +1035,7 @@ mod tests { ); assert_eq!( removed, - vec![(3, delete_ts_3)], + vec![(key(3), delete_ts_3)], "Expected set b to have key `3` marked as deleted." ); } @@ -1045,12 +1050,12 @@ mod tests { // This delete conflicts with the insert timestamp. // We expect node with the biggest ID to win. - node_a_set.insert(1, node_a); - node_b_set.delete(1, node_b); + node_a_set.insert(key(1), node_a); + node_b_set.delete(key(1), node_b); let (changed, removed) = node_a_set.diff(&node_b_set); assert_eq!(changed, vec![]); - assert_eq!(removed, vec![(1, node_b)]); + assert_eq!(removed, vec![(key(1), node_b)]); let (changed, removed) = node_b_set.diff(&node_a_set); assert_eq!(changed, vec![]); @@ -1060,10 +1065,10 @@ mod tests { node_b_set.merge(node_a_set.clone()); assert!( - node_a_set.get(&1).is_none(), + node_a_set.get(&key(1)).is_none(), "Set a should no longer have key 1." ); - assert!(node_b_set.get(&1).is_none(), "Set b should not have key 1."); + assert!(node_b_set.get(&key(1)).is_none(), "Set b should not have key 1."); let (changed, removed) = node_b_set.diff(&node_a_set); assert_eq!(changed, vec![]); @@ -1073,19 +1078,19 @@ mod tests { assert_eq!(changed, vec![]); assert_eq!(removed, vec![]); - let has_changed = node_a_set.insert(1, node_a); + let has_changed = node_a_set.insert(key(1), node_a); assert!(!has_changed, "Set a should not insert the value."); - let has_changed = node_b_set.insert(1, node_a); + let has_changed = node_b_set.insert(key(1), node_a); assert!( !has_changed, "Set b should not insert the value with node a's timestamp." ); - let has_changed = node_a_set.insert(1, node_b); + let has_changed = node_a_set.insert(key(1), node_b); assert!( has_changed, "Set a should insert the value with node b's timestamp." ); - let has_changed = node_b_set.insert(1, node_b); + let has_changed = node_b_set.insert(key(1), node_b); assert!(has_changed, "Set b should insert the value."); let mut node_a_set = OrSWotSet::<1>::default(); @@ -1093,11 +1098,11 @@ mod tests { // This delete conflicts with the insert timestamp. // We expect node with the biggest ID to win. - node_a_set.delete(1, node_a); - node_b_set.insert(1, node_b); + node_a_set.delete(key(1), node_a); + node_b_set.insert(key(1), node_b); let (changed, removed) = node_a_set.diff(&node_b_set); - assert_eq!(changed, vec![(1, node_b)]); + assert_eq!(changed, vec![(key(1), node_b)]); assert_eq!(removed, vec![]); let (changed, removed) = node_b_set.diff(&node_a_set); @@ -1108,10 +1113,10 @@ mod tests { node_b_set.merge(node_a_set.clone()); assert!( - node_a_set.get(&1).is_some(), + node_a_set.get(&key(1)).is_some(), "Set a should no longer have key 1." ); - assert!(node_b_set.get(&1).is_some(), "Set b should not have key 1."); + assert!(node_b_set.get(&key(1)).is_some(), "Set b should not have key 1."); } #[test] @@ -1120,12 +1125,12 @@ mod tests { let mut node_set = OrSWotSet::<1>::default(); // A basic example of the purging system. - node_set.insert_with_source(0, 1, clock.send().unwrap()); + node_set.insert_with_source(0, key(1), clock.send().unwrap()); - node_set.delete_with_source(0, 1, clock.send().unwrap()); + node_set.delete_with_source(0, key(1), clock.send().unwrap()); - node_set.insert_with_source(0, 3, clock.send().unwrap()); - node_set.insert_with_source(0, 4, clock.send().unwrap()); + node_set.insert_with_source(0, key(3), clock.send().unwrap()); + node_set.insert_with_source(0, key(4), clock.send().unwrap()); // Since we're only using one source here, we should be able to safely purge key `1`. let purged = node_set @@ -1133,20 +1138,20 @@ mod tests { .into_iter() .map(|(key, _)| key) .collect::>(); - assert_eq!(purged, vec![1]); + assert_eq!(purged, vec![key(1)]); let mut node_set = OrSWotSet::<2>::default(); // Insert a new entry from source `1` and `0`. - node_set.insert_with_source(0, 1, clock.send().unwrap()); - node_set.insert_with_source(1, 2, clock.send().unwrap()); + node_set.insert_with_source(0, key(1), clock.send().unwrap()); + node_set.insert_with_source(1, key(2), clock.send().unwrap()); // Delete an entry from the set. (Mark it as a tombstone.) - node_set.delete_with_source(0, 1, clock.send().unwrap()); + node_set.delete_with_source(0, key(1), clock.send().unwrap()); // Effectively 'observe' a new set of changes. - node_set.insert_with_source(0, 3, clock.send().unwrap()); - node_set.insert_with_source(0, 4, clock.send().unwrap()); + node_set.insert_with_source(0, key(3), clock.send().unwrap()); + node_set.insert_with_source(0, key(4), clock.send().unwrap()); // No keys should be purged, because source `1` has not changed it's last // observed timestamp, which means the system cannot guarantee that it is safe @@ -1155,7 +1160,7 @@ mod tests { assert!(purged.is_empty()); // Our other source has also now observed a new timestamp. - node_set.insert_with_source(1, 3, clock.send().unwrap()); + node_set.insert_with_source(1, key(3), clock.send().unwrap()); // We should now have successfully removed the key. let purged = node_set @@ -1163,24 +1168,24 @@ mod tests { .into_iter() .map(|(key, _)| key) .collect::>(); - assert_eq!(purged, vec![1]); + assert_eq!(purged, vec![key(1)]); let old_ts = clock.send().unwrap(); let initial_ts = clock.send().unwrap(); // Deletes from one source shouldn't affect deletes from the other. - assert!(node_set.delete_with_source(0, 4, initial_ts)); - assert!(node_set.delete_with_source(1, 3, old_ts)); - assert!(!node_set.delete_with_source(0, 3, old_ts)); - - assert!(node_set.insert_with_source(0, 5, initial_ts)); - assert!(node_set.insert_with_source(1, 6, old_ts)); - assert!(!node_set.insert_with_source(0, 5, old_ts)); - - assert!(node_set.insert_with_source(0, 6, initial_ts)); - assert!(node_set.delete_with_source(1, 4, clock.send().unwrap())); - assert!(node_set.delete_with_source(0, 3, clock.send().unwrap())); - assert!(!node_set.delete_with_source(1, 4, initial_ts)); + assert!(node_set.delete_with_source(0, key(4), initial_ts)); + assert!(node_set.delete_with_source(1, key(3), old_ts)); + assert!(!node_set.delete_with_source(0, key(3), old_ts)); + + assert!(node_set.insert_with_source(0, key(5), initial_ts)); + assert!(node_set.insert_with_source(1, key(6), old_ts)); + assert!(!node_set.insert_with_source(0, key(5), old_ts)); + + assert!(node_set.insert_with_source(0, key(6), initial_ts)); + assert!(node_set.delete_with_source(1, key(4), clock.send().unwrap())); + assert!(node_set.delete_with_source(0, key(3), clock.send().unwrap())); + assert!(!node_set.delete_with_source(1, key(4), initial_ts)); } #[test] @@ -1188,12 +1193,12 @@ mod tests { let ts = Duration::from_secs(1); let mut node_set = OrSWotSet::<1>::default(); - assert!(node_set.will_apply(1, HLCTimestamp::new(ts, 0, 0))); - node_set.insert(1, HLCTimestamp::new(ts, 0, 0)); - assert!(!node_set.will_apply(1, HLCTimestamp::new(ts, 0, 0))); + assert!(node_set.will_apply(key(1), HLCTimestamp::new(ts, 0, 0))); + node_set.insert(key(1), HLCTimestamp::new(ts, 0, 0)); + assert!(!node_set.will_apply(key(1), HLCTimestamp::new(ts, 0, 0))); - assert!(node_set.will_apply(3, HLCTimestamp::new(Duration::from_secs(3), 0, 0))); - node_set.delete(3, HLCTimestamp::new(Duration::from_secs(5), 0, 0)); - assert!(!node_set.will_apply(3, HLCTimestamp::new(Duration::from_secs(4), 0, 0))); + assert!(node_set.will_apply(key(3), HLCTimestamp::new(Duration::from_secs(3), 0, 0))); + node_set.delete(key(3), HLCTimestamp::new(Duration::from_secs(5), 0, 0)); + assert!(!node_set.will_apply(key(3), HLCTimestamp::new(Duration::from_secs(4), 0, 0))); } } diff --git a/datacake-eventual-consistency/Cargo.toml b/datacake-eventual-consistency/Cargo.toml index bcaf505..c28fcbb 100644 --- a/datacake-eventual-consistency/Cargo.toml +++ b/datacake-eventual-consistency/Cargo.toml @@ -15,8 +15,8 @@ readme = "README.md" tracing = "0.1.36" tokio-stream = "0.1.9" flume = "0.10.14" -futures = "0.3.23" -itertools = "0.10.3" +futures = "0.3.31" +itertools = "0.13" thiserror = "1" parking_lot = "0.12.1" crc32fast = "1.3.2" diff --git a/datacake-eventual-consistency/src/core.rs b/datacake-eventual-consistency/src/core.rs index f206089..8548fb1 100644 --- a/datacake-eventual-consistency/src/core.rs +++ b/datacake-eventual-consistency/src/core.rs @@ -10,7 +10,7 @@ use smallvec::SmallVec; pub(crate) type DocVec = SmallVec<[T; 4]>; #[repr(C)] -#[derive(Serialize, Deserialize, Archive, Copy, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Archive, Clone, Debug, PartialEq)] #[archive(check_bytes)] #[archive_attr(repr(C))] /// The metadata attached to each document. @@ -62,8 +62,8 @@ impl Document { #[inline] /// The unique id of the document. - pub fn id(&self) -> Key { - self.metadata.id + pub fn id(&self) -> &[u8] { + &self.metadata.id } #[inline] diff --git a/datacake-eventual-consistency/src/error.rs b/datacake-eventual-consistency/src/error.rs index 7b45635..2b8c19b 100644 --- a/datacake-eventual-consistency/src/error.rs +++ b/datacake-eventual-consistency/src/error.rs @@ -9,6 +9,22 @@ use thiserror::Error; use crate::storage::BulkMutationError; +#[derive(Debug, Error)] +/// An error that occurs when attempting to access a keyspace with a different key type +/// than it was created with. +pub enum TypeMismatchError { + #[error("Keyspace '{keyspace}' was created with key type '{expected}' but accessed with '{actual}'")] + /// The keyspace was previously accessed with a different key type. + KeyTypeMismatch { + /// The name of the keyspace that had a type mismatch. + keyspace: String, + /// The expected key type name. + expected: String, + /// The actual key type name that was used in the access attempt. + actual: String, + }, +} + #[derive(Debug, Error)] /// A wrapping error for the store which can potentially fail under situations. pub enum StoreError { diff --git a/datacake-eventual-consistency/src/keyspace/actor.rs b/datacake-eventual-consistency/src/keyspace/actor.rs index 0294223..1faae12 100644 --- a/datacake-eventual-consistency/src/keyspace/actor.rs +++ b/datacake-eventual-consistency/src/keyspace/actor.rs @@ -62,11 +62,11 @@ where /// If the document is not the newest the store has seen thus far, it is a no-op. async fn on_set(&mut self, msg: Set) -> Result<(), S::Error> { // We have something newer. - if !self.state.will_apply(msg.doc.id(), msg.doc.last_updated()) { + if !self.state.will_apply(msg.doc.id().to_vec(), msg.doc.last_updated()) { return Ok(()); } - let doc_id = msg.doc.id(); + let doc_id = msg.doc.id().to_vec(); let ts = msg.doc.last_updated(); self.storage @@ -93,9 +93,9 @@ where let docs = msg .docs .into_iter() - .filter(|doc| self.state.will_apply(doc.id(), doc.last_updated())) + .filter(|doc| self.state.will_apply(doc.id().to_vec(), doc.last_updated())) .map(|doc| { - valid_entries.push((doc.id(), doc.last_updated())); + valid_entries.push((doc.id().to_vec(), doc.last_updated())); doc }); @@ -109,7 +109,7 @@ where self.inc_change_timestamp().await; if let Err(error) = res { - let successful_ids = HashSet::<_>::from_iter(error.successful_doc_ids()); + let successful_ids = HashSet::<_>::from_iter(error.successful_doc_ids().iter().cloned()); let successful_entries = valid_entries .into_iter() .filter(|entry| successful_ids.contains(&entry.0)); @@ -132,17 +132,20 @@ where /// If the document is not the newest the store has seen thus far, it is a no-op. async fn on_del(&mut self, msg: Del) -> Result<(), S::Error> { // We have something newer. - if !self.state.will_apply(msg.doc.id, msg.doc.last_updated) { + if !self.state.will_apply(msg.doc.id.clone(), msg.doc.last_updated) { return Ok(()); } + let doc_id = msg.doc.id.clone(); + let ts = msg.doc.last_updated; + self.storage - .mark_as_tombstone(&self.name, msg.doc.id, msg.doc.last_updated) + .mark_as_tombstone(&self.name, msg.doc.id, ts) .await?; // The change has gone through, let's apply our memory state. self.state - .delete_with_source(msg.source, msg.doc.id, msg.doc.last_updated); + .delete_with_source(msg.source, doc_id, ts); self.inc_change_timestamp().await; Ok(()) } @@ -161,9 +164,9 @@ where let docs = msg .docs .into_iter() - .filter(|doc| self.state.will_apply(doc.id, doc.last_updated)) + .filter(|doc| self.state.will_apply(doc.id.clone(), doc.last_updated)) .map(|doc| { - valid_entries.push((doc.id, doc.last_updated)); + valid_entries.push((doc.id.clone(), doc.last_updated)); doc }); @@ -174,7 +177,7 @@ where self.inc_change_timestamp().await; if let Err(error) = res { - let successful_ids = HashSet::<_>::from_iter(error.successful_doc_ids()); + let successful_ids = HashSet::<_>::from_iter(error.successful_doc_ids().iter().cloned()); let successful_entries = valid_entries .into_iter() .filter(|entry| successful_ids.contains(&entry.0)); @@ -200,7 +203,7 @@ where let res = self .storage - .remove_tombstones(&self.name, changes.iter().map(|(key, _)| *key)) + .remove_tombstones(&self.name, changes.iter().map(|(key, _)| key.clone())) .await; // The operation may have been partially successful. We should re-mark @@ -272,6 +275,11 @@ mod tests { }}; } + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + async fn make_actor( clock: Clock, storage: MockStorage, @@ -290,9 +298,9 @@ mod tests { async fn test_on_set() { let clock = Clock::new(0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); let docs = [doc_1.clone(), doc_2.clone(), doc_3.clone()]; @@ -341,9 +349,9 @@ mod tests { let clock = Clock::new(0); let old_ts = HLCTimestamp::new(lag!(3_700), 0, 0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, old_ts, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), old_ts, b"Hello, world 3".to_vec()); let docs = [doc_1.clone(), doc_2.clone()]; @@ -395,9 +403,9 @@ mod tests { async fn test_on_multi_set() { let clock = Clock::new(0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); let docs = [doc_1.clone(), doc_2.clone(), doc_3.clone()]; @@ -429,13 +437,13 @@ mod tests { let clock = Clock::new(0); let old_ts = HLCTimestamp::new(lag!(3_700), 0, 0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); - let doc_4 = Document::new(4, old_ts, b"Hello, world 4".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_4 = Document::new(key(4), old_ts, b"Hello, world 4".to_vec()); - let doc_3_metadata = doc_3.metadata; - let doc_1_metadata = doc_1.metadata; + let doc_3_metadata = doc_3.metadata.clone(); + let doc_1_metadata = doc_1.metadata.clone(); let docs = [doc_2.clone()]; @@ -492,10 +500,10 @@ mod tests { let clock = Clock::new(0); let old_ts = HLCTimestamp::new(lag!(3_700), 0, 0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); - let doc_4 = Document::new(4, old_ts, b"Hello, world 4".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_4 = Document::new(key(4), old_ts, b"Hello, world 4".to_vec()); let docs = [doc_4.clone(), doc_2.clone(), doc_1.clone(), doc_3.clone()]; @@ -528,21 +536,22 @@ mod tests { .await .expect("Put operation should be successful."); - assert!(keyspace.state.get(&doc_1.id()).is_some()); - assert!(keyspace.state.get(&doc_2.id()).is_some()); - assert!(keyspace.state.get(&doc_3.id()).is_some()); - assert!(keyspace.state.get(&doc_4.id()).is_some()); + assert!(keyspace.state.get(&doc_1.id().to_vec()).is_some()); + assert!(keyspace.state.get(&doc_2.id().to_vec()).is_some()); + assert!(keyspace.state.get(&doc_3.id().to_vec()).is_some()); + assert!(keyspace.state.get(&doc_4.id().to_vec()).is_some()); } #[tokio::test] async fn test_on_del() { let clock = Clock::new(0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); let docs = [doc_1.clone(), doc_2.clone(), doc_3.clone()]; + let doc_1_id = doc_1.metadata.id.clone(); let delete_ts = clock.get_time().await; let mock_store = MockStorage::default() @@ -555,7 +564,7 @@ mod tests { }) .expect_mark_as_tombstone(1, move |keyspace, doc_id, ts| { assert_eq!(keyspace, "my-keyspace"); - assert_eq!(doc_id, doc_1.metadata.id); + assert_eq!(doc_id, doc_1_id); assert_eq!(ts, delete_ts); Ok(()) @@ -576,7 +585,7 @@ mod tests { .on_del(Del { source: 0, doc: DocumentMetadata { - id: doc_1.id(), + id: doc_1.id().to_vec(), last_updated: delete_ts, }, _marker: Default::default(), @@ -589,11 +598,11 @@ mod tests { async fn test_on_multi_del() { let clock = Clock::new(0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); - let doc_ids = [doc_1.id(), doc_2.id()]; + let doc_ids = [doc_1.id().to_vec(), doc_2.id().to_vec()]; let docs = [doc_1.clone(), doc_2.clone(), doc_3.clone()]; let mock_store = MockStorage::default() @@ -626,8 +635,8 @@ mod tests { .on_multi_del(MultiDel { source: 0, docs: smallvec![ - DocumentMetadata::new(doc_1.id(), clock.get_time().await), - DocumentMetadata::new(doc_2.id(), clock.get_time().await), + DocumentMetadata::new(doc_1.id().to_vec(), clock.get_time().await), + DocumentMetadata::new(doc_2.id().to_vec(), clock.get_time().await), ], _marker: Default::default(), }) @@ -639,15 +648,15 @@ mod tests { async fn test_on_del_unordered_events() { let clock = Clock::new(0); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); let docs = [doc_1.clone(), doc_2.clone(), doc_3.clone()]; let deletes_expected = smallvec![ - DocumentMetadata::new(doc_3.id(), clock.get_time().await), - DocumentMetadata::new(doc_1.id(), clock.get_time().await), + DocumentMetadata::new(doc_3.id().to_vec(), clock.get_time().await), + DocumentMetadata::new(doc_1.id().to_vec(), clock.get_time().await), ]; let deletes_expected_clone = deletes_expected.clone(); @@ -686,25 +695,25 @@ mod tests { .await .expect("Put operation should be successful."); - assert!(keyspace.state.get(&doc_2.id()).is_some()); - assert!(!keyspace.state.delete(doc_1.id(), doc_1.last_updated())); - assert!(!keyspace.state.delete(doc_3.id(), doc_3.last_updated())); + assert!(keyspace.state.get(&doc_2.id().to_vec()).is_some()); + assert!(!keyspace.state.delete(doc_1.id().to_vec(), doc_1.last_updated())); + assert!(!keyspace.state.delete(doc_3.id().to_vec(), doc_3.last_updated())); // Push the safe timestamp forwards. keyspace .state - .insert_with_source(1, 5, HLCTimestamp::new(drift!(3700), 0, 0)); + .insert_with_source(1, key(5), HLCTimestamp::new(drift!(3700), 0, 0)); keyspace .state - .insert_with_source(0, 2, HLCTimestamp::new(drift!(3700), 1, 0)); + .insert_with_source(0, key(2), HLCTimestamp::new(drift!(3700), 1, 0)); let mut changes = keyspace.state.purge_old_deletes(); // Needed because it may not be ordered. - changes.sort_by_key(|change| Reverse(change.0)); + changes.sort_by_key(|change| Reverse(change.0.clone())); let expected_deletes = deletes_expected .iter() - .map(|doc| (doc.id, doc.last_updated)) + .map(|doc| (doc.id.clone(), doc.last_updated)) .collect::>(); assert_eq!(changes, expected_deletes); } diff --git a/datacake-eventual-consistency/src/keyspace/group.rs b/datacake-eventual-consistency/src/keyspace/group.rs index 2e68bd9..579d630 100644 --- a/datacake-eventual-consistency/src/keyspace/group.rs +++ b/datacake-eventual-consistency/src/keyspace/group.rs @@ -14,6 +14,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use tokio::time::interval; use super::NUM_SOURCES; +use crate::error::TypeMismatchError; use crate::keyspace::messages::PurgeDeletes; use crate::keyspace::KeyspaceActor; use crate::Storage; @@ -32,6 +33,7 @@ where clock: Clock, storage: Arc, keyspace_timestamps: Arc>, + keyspace_types: Arc>, group: Arc>>, } @@ -44,6 +46,7 @@ where clock: self.clock.clone(), storage: self.storage.clone(), keyspace_timestamps: self.keyspace_timestamps.clone(), + keyspace_types: self.keyspace_types.clone(), group: self.group.clone(), } } @@ -73,6 +76,7 @@ where clock, storage, keyspace_timestamps: Default::default(), + keyspace_types: Default::default(), group: Default::default(), }; @@ -336,11 +340,62 @@ impl DerefMut for KeyspaceTimestamps { } } +/// Tracks the key type for each keyspace to ensure type consistency. +#[derive(Default, Clone, Debug)] +pub struct KeyspaceTypeInfo(pub HashMap); + +impl KeyspaceTypeInfo { + /// Registers a new keyspace with its associated key type. + /// + /// If the keyspace already exists with a different type, returns an error. + pub fn register_or_validate( + &mut self, + keyspace: &str, + type_name: &str, + ) -> Result<(), TypeMismatchError> { + if let Some(existing_type) = self.0.get(keyspace) { + if existing_type != type_name { + return Err(TypeMismatchError::KeyTypeMismatch { + keyspace: keyspace.to_string(), + expected: existing_type.clone(), + actual: type_name.to_string(), + }); + } + } else { + self.0.insert(keyspace.to_string(), type_name.to_string()); + } + Ok(()) + } +} + +impl KeyspaceGroup +where + S: Storage, +{ + /// Registers or validates the key type for a given keyspace. + /// + /// This ensures that a keyspace is always accessed with the same key type + /// throughout the lifetime of the process. + pub fn register_keyspace_type( + &self, + keyspace: &str, + type_name: &str, + ) -> Result<(), TypeMismatchError> { + let mut guard = self.keyspace_types.write(); + guard.register_or_validate(keyspace, type_name) + } +} + #[cfg(test)] mod tests { use super::*; use crate::test_utils::MockStorage; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + #[tokio::test] async fn test_groups_load_from_blank_storage() { let storage = @@ -360,10 +415,10 @@ mod tests { "keyspace-4".to_string(), ]; let metadata = vec![ - (1, HLCTimestamp::new(Duration::from_secs(1), 0, 0), false), - (2, HLCTimestamp::new(Duration::from_secs(2), 0, 0), false), - (3, HLCTimestamp::new(Duration::from_secs(3), 3, 0), true), - (4, HLCTimestamp::new(Duration::from_secs(4), 0, 0), false), + (key(1), HLCTimestamp::new(Duration::from_secs(1), 0, 0), false), + (key(2), HLCTimestamp::new(Duration::from_secs(2), 0, 0), false), + (key(3), HLCTimestamp::new(Duration::from_secs(3), 3, 0), true), + (key(4), HLCTimestamp::new(Duration::from_secs(4), 0, 0), false), ]; let keyspace_list_clone = keyspace_list.clone(); diff --git a/datacake-eventual-consistency/src/lib.rs b/datacake-eventual-consistency/src/lib.rs index b11b024..01eb965 100644 --- a/datacake-eventual-consistency/src/lib.rs +++ b/datacake-eventual-consistency/src/lib.rs @@ -31,19 +31,22 @@ //! .add_extension(EventuallyConsistentStoreExtension::new(MemStore::default())) //! .await //! .expect("Create store."); -//! +//! //! let handle = store.handle(); //! +//! // Helper to convert u64 to Vec key +//! let key = |n: u64| n.to_le_bytes().to_vec(); +//! //! handle //! .put( //! "my-keyspace", -//! 1, +//! key(1), //! b"Hello, world! From keyspace 1.".to_vec(), //! Consistency::All, //! ) //! .await //! .expect("Put doc."); -//! +//! //! Ok(()) //! } //! ``` @@ -61,6 +64,7 @@ mod replication; mod rpc; mod statistics; mod storage; +mod typed_handle; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; @@ -72,7 +76,7 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use datacake_crdt::Key; +use datacake_crdt::{DatacakeKey, Key}; use datacake_node::{ ClusterExtension, Consistency, @@ -81,7 +85,7 @@ use datacake_node::{ DatacakeNode, Nodes, }; -pub use error::StoreError; +pub use error::{StoreError, TypeMismatchError}; use futures::stream::FuturesUnordered; use futures::StreamExt; pub use statistics::SystemStatistics; @@ -90,6 +94,7 @@ pub use storage::test_suite; pub use storage::{BulkMutationError, ProgressTracker, PutContext, Storage}; pub use self::core::{Document, DocumentMetadata}; +pub use self::typed_handle::TypedKeyspaceHandle; use crate::core::DocVec; use crate::keyspace::{ Del, @@ -274,6 +279,56 @@ where keyspace: Cow::Owned(keyspace.into()), } } + + /// Creates a new type-safe handle to the underlying storage system with a preset keyspace. + /// + /// This method registers the key type for the keyspace and validates that all future + /// accesses use the same key type. If the keyspace has already been accessed with a + /// different key type, this returns a `TypeMismatchError`. + /// + /// # Type Parameters + /// + /// * `K` - The key type that implements `DatacakeKey`. Common types include: + /// - `u64` for numeric keys + /// - `String` for text-based keys + /// - `uuid::Uuid` for UUID keys (requires `uuid` feature) + /// - `(String, String)` for composite keys + /// - Custom types implementing `DatacakeKey` + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::EventuallyConsistentStore; + /// # use datacake_eventual_consistency::test_utils::MemStore; + /// # async fn example(store: EventuallyConsistentStore) -> Result<(), Box> { + /// // Create a handle with String keys + /// let users = store.typed_handle::("users")?; + /// + /// // Create a handle with u64 keys + /// let counters = store.typed_handle::("counters")?; + /// + /// // This would fail if we try to access "users" with a different type: + /// // let fail = store.typed_handle::("users")?; // Error! + /// # Ok(()) + /// # } + /// ``` + pub fn typed_handle( + &self, + keyspace: impl Into, + ) -> Result, TypeMismatchError> + where + K: DatacakeKey, + { + let keyspace_str = keyspace.into(); + let type_name = std::any::type_name::(); + + // Register or validate the key type for this keyspace + self.group.register_keyspace_type(&keyspace_str, type_name)?; + + // Create the untyped handle and wrap it + let untyped = self.handle_with_keyspace(keyspace_str); + Ok(TypedKeyspaceHandle::new(untyped)) + } } impl Drop for EventuallyConsistentStore @@ -334,6 +389,45 @@ where } } + /// Creates a new type-safe handle to the underlying storage system with a preset keyspace. + /// + /// This method registers the key type for the keyspace and validates that all future + /// accesses use the same key type. If the keyspace has already been accessed with a + /// different key type, this returns a `TypeMismatchError`. + /// + /// # Type Parameters + /// + /// * `K` - The key type that implements `DatacakeKey` + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::ReplicatedStoreHandle; + /// # use datacake_eventual_consistency::test_utils::MemStore; + /// # async fn example(handle: ReplicatedStoreHandle) -> Result<(), Box> { + /// // Create a typed handle with String keys + /// let users = handle.typed_keyspace::("users")?; + /// # Ok(()) + /// # } + /// ``` + pub fn typed_keyspace( + &self, + keyspace: impl Into, + ) -> Result, TypeMismatchError> + where + K: DatacakeKey, + { + let keyspace_str = keyspace.into(); + let type_name = std::any::type_name::(); + + // Register or validate the key type for this keyspace + self.group.register_keyspace_type(&keyspace_str, type_name)?; + + // Create the untyped handle and wrap it + let untyped = self.with_keyspace(keyspace_str); + Ok(TypedKeyspaceHandle::new(untyped)) + } + /// Retrieves the list of keyspaces from the underlying storage. pub async fn get_keyspace_list(&self) -> Result, S::Error> { let storage = self.group.storage(); @@ -520,12 +614,12 @@ where let keyspace = self.group.get_or_create_keyspace(keyspace).await; let doc = DocumentMetadata { - id: doc_id, + id: doc_id.clone(), last_updated, }; let msg = Del { source: CONSISTENCY_SOURCE_ID, - doc, + doc: doc.clone(), _marker: PhantomData::::default(), }; keyspace.send(msg).await?; @@ -539,6 +633,7 @@ where let factory = |node| { let clock = self.node.clock().clone(); let keyspace = keyspace.name().to_string(); + let doc_id = doc_id.clone(); async move { let channel = self.node.network().get_or_connect(node); diff --git a/datacake-eventual-consistency/src/replication/poller.rs b/datacake-eventual-consistency/src/replication/poller.rs index 312b499..ed17b79 100644 --- a/datacake-eventual-consistency/src/replication/poller.rs +++ b/datacake-eventual-consistency/src/replication/poller.rs @@ -453,7 +453,7 @@ where { let doc_id_chunks = modified .chunks(MAX_NUMBER_OF_DOCS_PER_FETCH) - .map(|entries| entries.iter().map(|doc| doc.id).collect::>()); + .map(|entries| entries.iter().map(|doc| doc.id.clone()).collect::>()); let total = Instant::now(); let mut total_num_docs = 0; diff --git a/datacake-eventual-consistency/src/rpc/client.rs b/datacake-eventual-consistency/src/rpc/client.rs index 2017c52..5e6b927 100644 --- a/datacake-eventual-consistency/src/rpc/client.rs +++ b/datacake-eventual-consistency/src/rpc/client.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::net::SocketAddr; -use datacake_crdt::{HLCTimestamp, Key, OrSWotSet}; +use datacake_crdt::{HLCTimestamp, OrSWotSet}; use datacake_node::{Clock, NodeId}; use datacake_rpc::{Channel, RpcClient, Status}; @@ -98,7 +98,7 @@ where pub async fn del( &mut self, keyspace: impl Into, - id: Key, + id: Vec, ts: HLCTimestamp, ) -> Result<(), Status> { let timestamp = self.clock.get_time().await; @@ -224,7 +224,7 @@ where pub async fn fetch_docs( &mut self, keyspace: impl Into, - doc_ids: Vec, + doc_ids: Vec>, ) -> Result, Status> { let timestamp = self.clock.get_time().await; let inner = self diff --git a/datacake-eventual-consistency/src/rpc/services/consistency_impl.rs b/datacake-eventual-consistency/src/rpc/services/consistency_impl.rs index 6de5496..144271f 100644 --- a/datacake-eventual-consistency/src/rpc/services/consistency_impl.rs +++ b/datacake-eventual-consistency/src/rpc/services/consistency_impl.rs @@ -304,6 +304,11 @@ mod tests { use super::*; use crate::test_utils::MemStore; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + #[tokio::test] async fn test_consistency_put() { static KEYSPACE: &str = "put-keyspace"; @@ -312,7 +317,7 @@ mod tests { let storage = group.storage(); let service = ConsistencyService::new(group.clone(), RpcNetwork::default()); - let doc = Document::new(1, clock.get_time().await, b"Hello, world".to_vec()); + let doc = Document::new(key(1), clock.get_time().await, b"Hello, world".to_vec()); let put_req = Request::using_owned(PutPayload { keyspace: KEYSPACE.to_string(), document: doc.clone(), @@ -327,7 +332,7 @@ mod tests { .expect("Put request should succeed."); let saved_doc = storage - .get(KEYSPACE, doc.id()) + .get(KEYSPACE, doc.id().to_vec()) .await .expect("Get new doc.") .expect("Doc should not be None"); @@ -338,7 +343,7 @@ mod tests { .await .expect("Iter metadata") .collect::>(); - assert_eq!(metadata, vec![(doc.id(), doc.last_updated(), false)]); + assert_eq!(metadata, vec![(doc.id().to_vec(), doc.last_updated(), false)]); } #[tokio::test] @@ -349,9 +354,9 @@ mod tests { let storage = group.storage(); let service = ConsistencyService::new(group.clone(), RpcNetwork::default()); - let doc_1 = Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); - let doc_2 = Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + let doc_1 = Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); + let doc_2 = Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); let put_req = Request::using_owned(MultiPutPayload { keyspace: KEYSPACE.to_string(), ctx: None, @@ -368,7 +373,7 @@ mod tests { let saved_docs = storage .multi_get( KEYSPACE, - vec![doc_1.id(), doc_2.id(), doc_3.id()].into_iter(), + [doc_1.id().to_vec(), doc_2.id().to_vec(), doc_3.id().to_vec()].into_iter(), ) .await .expect("Get new doc.") @@ -387,9 +392,9 @@ mod tests { assert_eq!( metadata, HashSet::from_iter([ - (doc_1.id(), doc_1.last_updated(), false), - (doc_2.id(), doc_2.last_updated(), false), - (doc_3.id(), doc_3.last_updated(), false), + (doc_1.id().to_vec(), doc_1.last_updated(), false), + (doc_2.id().to_vec(), doc_2.last_updated(), false), + (doc_3.id().to_vec(), doc_3.last_updated(), false), ]) ); } @@ -403,7 +408,7 @@ mod tests { let service = ConsistencyService::new(group.clone(), RpcNetwork::default()); let mut doc = - Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); + Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); add_docs( KEYSPACE, smallvec![doc.clone()], @@ -413,7 +418,7 @@ mod tests { .await; let saved_doc = storage - .get(KEYSPACE, doc.id()) + .get(KEYSPACE, doc.id().to_vec()) .await .expect("Get new doc.") .expect("Doc should not be None"); @@ -422,7 +427,7 @@ mod tests { doc.metadata.last_updated = clock.get_time().await; let remove_req = Request::using_owned(RemovePayload { keyspace: KEYSPACE.to_string(), - document: doc.metadata, + document: doc.metadata.clone(), timestamp: clock.get_time().await, }) .await; @@ -432,7 +437,7 @@ mod tests { .await .expect("Remove document."); - let saved_doc = storage.get(KEYSPACE, doc.id()).await.expect("Get new doc."); + let saved_doc = storage.get(KEYSPACE, doc.id().to_vec()).await.expect("Get new doc."); assert!(saved_doc.is_none(), "Documents should no longer exist."); let metadata = storage @@ -440,7 +445,7 @@ mod tests { .await .expect("Iter metadata") .collect::>(); - assert_eq!(metadata, vec![(doc.id(), doc.last_updated(), true)]); + assert_eq!(metadata, vec![(doc.id().to_vec(), doc.last_updated(), true)]); } #[tokio::test] @@ -452,10 +457,10 @@ mod tests { let service = ConsistencyService::new(group.clone(), RpcNetwork::default()); let mut doc_1 = - Document::new(1, clock.get_time().await, b"Hello, world 1".to_vec()); + Document::new(key(1), clock.get_time().await, b"Hello, world 1".to_vec()); let mut doc_2 = - Document::new(2, clock.get_time().await, b"Hello, world 2".to_vec()); - let doc_3 = Document::new(3, clock.get_time().await, b"Hello, world 3".to_vec()); + Document::new(key(2), clock.get_time().await, b"Hello, world 2".to_vec()); + let doc_3 = Document::new(key(3), clock.get_time().await, b"Hello, world 3".to_vec()); add_docs( KEYSPACE, smallvec![doc_1.clone(), doc_2.clone(), doc_3.clone()], @@ -467,7 +472,7 @@ mod tests { let saved_docs = storage .multi_get( KEYSPACE, - vec![doc_1.id(), doc_2.id(), doc_3.id()].into_iter(), + [doc_1.id().to_vec(), doc_2.id().to_vec(), doc_3.id().to_vec()].into_iter(), ) .await .expect("Get new doc.") @@ -482,7 +487,7 @@ mod tests { doc_2.metadata.last_updated = clock.get_time().await; let remove_req = Request::using_owned(MultiRemovePayload { keyspace: KEYSPACE.to_string(), - documents: smallvec![doc_1.metadata, doc_2.metadata], + documents: smallvec![doc_1.metadata.clone(), doc_2.metadata.clone()], timestamp: clock.get_time().await, }) .await; @@ -495,7 +500,7 @@ mod tests { let saved_docs = storage .multi_get( KEYSPACE, - vec![doc_1.id(), doc_2.id(), doc_3.id()].into_iter(), + [doc_1.id().to_vec(), doc_2.id().to_vec(), doc_3.id().to_vec()].into_iter(), ) .await .expect("Get new doc.") @@ -514,9 +519,9 @@ mod tests { assert_eq!( metadata, HashSet::from_iter([ - (doc_1.id(), doc_1.last_updated(), true), - (doc_2.id(), doc_2.last_updated(), true), - (doc_3.id(), doc_3.last_updated(), false), + (doc_1.id().to_vec(), doc_1.last_updated(), true), + (doc_2.id().to_vec(), doc_2.last_updated(), true), + (doc_3.id().to_vec(), doc_3.last_updated(), false), ]) ); } diff --git a/datacake-eventual-consistency/src/rpc/services/replication_impl.rs b/datacake-eventual-consistency/src/rpc/services/replication_impl.rs index 6748260..6166576 100644 --- a/datacake-eventual-consistency/src/rpc/services/replication_impl.rs +++ b/datacake-eventual-consistency/src/rpc/services/replication_impl.rs @@ -1,4 +1,4 @@ -use datacake_crdt::{HLCTimestamp, Key}; +use datacake_crdt::HLCTimestamp; use datacake_rpc::{Handler, Request, RpcService, ServiceRegistry, Status}; use rkyv::{Archive, Deserialize, Serialize}; @@ -95,7 +95,7 @@ where if msg.doc_ids.len() == 1 { let documents = storage - .get(&msg.keyspace, msg.doc_ids[0]) + .get(&msg.keyspace, msg.doc_ids[0].clone()) .await .map_err(|e| Status::internal(e.to_string()))? .map(|doc| vec![doc]) @@ -152,8 +152,7 @@ pub struct KeyspaceOrSwotSet { #[archive(check_bytes)] pub struct FetchDocs { pub keyspace: String, - #[with(rkyv::with::Raw)] - pub doc_ids: Vec, + pub doc_ids: Vec>, pub timestamp: HLCTimestamp, } @@ -175,6 +174,11 @@ mod tests { use crate::test_utils::MemStore; use crate::Document; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + #[tokio::test] async fn test_poll_keyspace() { static KEYSPACE: &str = "poll-keyspace"; @@ -223,7 +227,7 @@ mod tests { keyspace .send(Set { source: READ_REPAIR_SOURCE_ID, - doc: Document::new(1, clock.get_time().await, Vec::new()), + doc: Document::new(key(1), clock.get_time().await, Vec::new()), ctx: None, _marker: PhantomData::::default(), }) @@ -265,7 +269,7 @@ mod tests { let keyspace = group.get_or_create_keyspace(KEYSPACE).await; - let doc = Document::new(1, clock.get_time().await, b"Hello, world".to_vec()); + let doc = Document::new(key(1), clock.get_time().await, b"Hello, world".to_vec()); keyspace .send(Set { source: READ_REPAIR_SOURCE_ID, @@ -280,7 +284,7 @@ mod tests { let fetch_docs_req = Request::using_owned(FetchDocs { timestamp, keyspace: KEYSPACE.to_string(), - doc_ids: vec![1], + doc_ids: vec![key(1)], }) .await; diff --git a/datacake-eventual-consistency/src/storage.rs b/datacake-eventual-consistency/src/storage.rs index 3c3fc35..77eb1f3 100644 --- a/datacake-eventual-consistency/src/storage.rs +++ b/datacake-eventual-consistency/src/storage.rs @@ -354,6 +354,11 @@ pub mod test_suite { use crate::storage::Storage; use crate::{BulkMutationError, DocumentMetadata, PutContext}; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + /// A wrapping type around another `Storage` implementation that /// logs all the activity going into and out of the store. /// @@ -443,7 +448,7 @@ pub mod test_suite { doc_id: Key, timestamp: HLCTimestamp, ) -> Result<(), Self::Error> { - info!(keyspace = keyspace, doc_id = doc_id, timestamp = %timestamp, "mark_as_tombstone"); + info!(keyspace = keyspace, doc_id = ?doc_id, timestamp = %timestamp, "mark_as_tombstone"); self.0.mark_as_tombstone(keyspace, doc_id, timestamp).await } @@ -464,7 +469,7 @@ pub mod test_suite { keyspace: &str, doc_id: Key, ) -> Result, Self::Error> { - info!(keyspace = keyspace, doc_id = doc_id, "get"); + info!(keyspace = keyspace, doc_id = ?doc_id, "get"); self.0.get(keyspace, doc_id).await } @@ -526,7 +531,7 @@ pub mod test_suite { .collect::>(); assert_eq!(metadata, to_hashset([]), "New keyspace should be empty."); - let doc = Document::new(1, clock.send().unwrap(), Vec::new()); + let doc = Document::new(key(1), clock.send().unwrap(), Vec::new()); let res = storage.put_with_ctx(KEYSPACE, doc, None).await; assert!( res.is_ok(), @@ -534,7 +539,7 @@ pub mod test_suite { res ); - let doc = Document::new(2, clock.send().unwrap(), Vec::new()); + let doc = Document::new(key(2), clock.send().unwrap(), Vec::new()); let res = storage.put_with_ctx(KEYSPACE, doc, None).await; assert!( res.is_ok(), @@ -580,9 +585,9 @@ pub mod test_suite { static KEYSPACE: &str = "metadata-test-keyspace"; - let mut doc_1 = Document::new(1, clock.send().unwrap(), Vec::new()); - let mut doc_2 = Document::new(2, clock.send().unwrap(), Vec::new()); - let mut doc_3 = Document::new(3, clock.send().unwrap(), Vec::new()); + let mut doc_1 = Document::new(key(1), clock.send().unwrap(), Vec::new()); + let mut doc_2 = Document::new(key(2), clock.send().unwrap(), Vec::new()); + let mut doc_3 = Document::new(key(3), clock.send().unwrap(), Vec::new()); storage .multi_put( KEYSPACE, @@ -593,7 +598,7 @@ pub mod test_suite { doc_3.metadata.last_updated = clock.send().unwrap(); storage - .mark_as_tombstone(KEYSPACE, doc_3.id(), doc_3.last_updated()) + .mark_as_tombstone(KEYSPACE, doc_3.id().to_vec(), doc_3.last_updated()) .await .expect("Mark document as tombstone."); @@ -605,9 +610,9 @@ pub mod test_suite { assert_eq!( metadata, to_hashset([ - (doc_1.id(), doc_1.last_updated(), false), - (doc_2.id(), doc_2.last_updated(), false), - (doc_3.id(), doc_3.last_updated(), true), + (doc_1.id().to_vec(), doc_1.last_updated(), false), + (doc_2.id().to_vec(), doc_2.last_updated(), false), + (doc_3.id().to_vec(), doc_3.last_updated(), true), ]), "Persisted metadata entries should match expected values." ); @@ -617,7 +622,7 @@ pub mod test_suite { storage .mark_many_as_tombstone( KEYSPACE, - [doc_1.metadata, doc_2.metadata].into_iter(), + [doc_1.metadata.clone(), doc_2.metadata.clone()].into_iter(), ) .await .expect("Mark documents as tombstones."); @@ -629,15 +634,15 @@ pub mod test_suite { assert_eq!( metadata, to_hashset([ - (doc_1.id(), doc_1.last_updated(), true), - (doc_2.id(), doc_2.last_updated(), true), - (doc_3.id(), doc_3.last_updated(), true), + (doc_1.id().to_vec(), doc_1.last_updated(), true), + (doc_2.id().to_vec(), doc_2.last_updated(), true), + (doc_3.id().to_vec(), doc_3.last_updated(), true), ]), "Persisted metadata entries should match expected values." ); storage - .remove_tombstones(KEYSPACE, [1, 2].into_iter()) + .remove_tombstones(KEYSPACE, [key(1), key(2)].into_iter()) .await .expect("Remove tombstone entries."); let metadata = storage @@ -647,7 +652,7 @@ pub mod test_suite { .collect::>(); assert_eq!( metadata, - to_hashset([(doc_3.id(), doc_3.last_updated(), true)]), + to_hashset([(doc_3.id().to_vec(), doc_3.last_updated(), true)]), "Persisted metadata entries should match expected values after removal." ); @@ -669,9 +674,9 @@ pub mod test_suite { assert_eq!( metadata, to_hashset([ - (doc_1.id(), doc_1.last_updated(), false), - (doc_2.id(), doc_2.last_updated(), false), - (doc_3.id(), doc_3.last_updated(), false), + (doc_1.id().to_vec(), doc_1.last_updated(), false), + (doc_2.id().to_vec(), doc_2.last_updated(), false), + (doc_3.id().to_vec(), doc_3.last_updated(), false), ]), "Persisted metadata entries should match expected values after update." ); @@ -682,12 +687,12 @@ pub mod test_suite { storage .mark_many_as_tombstone( KEYSPACE, - [doc_1.metadata, doc_2.metadata, doc_3.metadata].into_iter(), + [doc_1.metadata.clone(), doc_2.metadata.clone(), doc_3.metadata.clone()].into_iter(), ) .await .expect("Mark documents as tombstones."); let res = storage - .remove_tombstones(KEYSPACE, [1, 2, 3].into_iter()) + .remove_tombstones(KEYSPACE, [key(1), key(2), key(3)].into_iter()) .await; assert!( res.is_ok(), @@ -713,10 +718,10 @@ pub mod test_suite { .mark_many_as_tombstone( KEYSPACE, [ - doc_1.metadata, - doc_2.metadata, - doc_3.metadata, - DocumentMetadata::new(4, doc_4_ts), + doc_1.metadata.clone(), + doc_2.metadata.clone(), + doc_3.metadata.clone(), + DocumentMetadata::new(key(4), doc_4_ts), ] .into_iter(), ) @@ -730,10 +735,10 @@ pub mod test_suite { assert_eq!( metadata, to_hashset([ - (doc_1.id(), doc_1.last_updated(), true), - (doc_2.id(), doc_2.last_updated(), true), - (doc_3.id(), doc_3.last_updated(), true), - (4, doc_4_ts, true), + (doc_1.id().to_vec(), doc_1.last_updated(), true), + (doc_2.id().to_vec(), doc_2.last_updated(), true), + (doc_3.id().to_vec(), doc_3.last_updated(), true), + (key(4), doc_4_ts, true), ]), "Persisted tombstones should be tracked." ); @@ -748,7 +753,7 @@ pub mod test_suite { static KEYSPACE: &str = "persistence-test-keyspace"; - let res = storage.get(KEYSPACE, 1).await; + let res = storage.get(KEYSPACE, key(1)).await; assert!( res.is_ok(), "Expected successful get request. Got: {:?}", @@ -761,22 +766,22 @@ pub mod test_suite { #[allow(clippy::needless_collect)] let res = storage - .multi_get(KEYSPACE, [1, 2, 3].into_iter()) + .multi_get(KEYSPACE, [key(1), key(2), key(3)].into_iter()) .await .expect("Expected successful get request.") .collect::>(); assert!(res.is_empty(), "Expected no document to be returned."); let mut doc_1 = - Document::new(1, clock.send().unwrap(), b"Hello, world!".to_vec()); - let mut doc_2 = Document::new(2, clock.send().unwrap(), Vec::new()); + Document::new(key(1), clock.send().unwrap(), b"Hello, world!".to_vec()); + let mut doc_2 = Document::new(key(2), clock.send().unwrap(), Vec::new()); let mut doc_3 = Document::new( - 3, + key(3), clock.send().unwrap(), b"Hello, from document 3!".to_vec(), ); let doc_3_updated = Document::new( - 3, + key(3), clock.send().unwrap(), b"Hello, from document 3 With an update!".to_vec(), ); @@ -785,7 +790,7 @@ pub mod test_suite { .put_with_ctx(KEYSPACE, doc_1.clone(), None) .await .expect("Put document in persistent store."); - let res = storage.get(KEYSPACE, 1).await; + let res = storage.get(KEYSPACE, key(1)).await; assert!( res.is_ok(), "Expected successful get request. Got: {:?}", @@ -801,7 +806,7 @@ pub mod test_suite { .await .expect("Put document in persistent store."); let res = storage - .multi_get(KEYSPACE, [1, 2, 3].into_iter()) + .multi_get(KEYSPACE, [key(1), key(2), key(3)].into_iter()) .await .expect("Expected successful get request.") .collect::>(); @@ -816,7 +821,7 @@ pub mod test_suite { .await .expect("Put updated document in persistent store."); let res = storage - .get(KEYSPACE, 3) + .get(KEYSPACE, key(3)) .await .expect("Get updated document."); let doc = res.expect("Expected document to be returned after updating doc."); @@ -824,10 +829,10 @@ pub mod test_suite { doc_2.metadata.last_updated = clock.send().unwrap(); storage - .mark_as_tombstone(KEYSPACE, doc_2.id(), doc_2.last_updated()) + .mark_as_tombstone(KEYSPACE, doc_2.id().to_vec(), doc_2.last_updated()) .await .expect("Mark document as tombstone."); - let res = storage.get(KEYSPACE, 2).await; + let res = storage.get(KEYSPACE, key(2)).await; assert!( res.is_ok(), "Expected successful get request. Got: {:?}", @@ -846,14 +851,14 @@ pub mod test_suite { [ doc_1.metadata, doc_2.metadata, - DocumentMetadata::new(4, clock.send().unwrap()), + DocumentMetadata::new(key(4), clock.send().unwrap()), ] .into_iter(), ) .await .expect("Merk documents as tombstones"); let res = storage - .multi_get(KEYSPACE, [1, 2, 3].into_iter()) + .multi_get(KEYSPACE, [key(1), key(2), key(3)].into_iter()) .await .expect("Expected successful get request.") .collect::>(); @@ -865,12 +870,12 @@ pub mod test_suite { doc_3.metadata.last_updated = clock.send().unwrap(); storage - .mark_as_tombstone(KEYSPACE, doc_3.id(), doc_3.last_updated()) + .mark_as_tombstone(KEYSPACE, doc_3.id().to_vec(), doc_3.last_updated()) .await .expect("Delete documents from store."); #[allow(clippy::needless_collect)] let res = storage - .multi_get(KEYSPACE, [1, 2, 3].into_iter()) + .multi_get(KEYSPACE, [key(1), key(2), key(3)].into_iter()) .await .expect("Expected successful get request.") .collect::>(); diff --git a/datacake-eventual-consistency/src/test_utils.rs b/datacake-eventual-consistency/src/test_utils.rs index 5f7e01d..1a8cf51 100644 --- a/datacake-eventual-consistency/src/test_utils.rs +++ b/datacake-eventual-consistency/src/test_utils.rs @@ -304,7 +304,7 @@ impl Storage for MemStore { if let Some(ks) = self.metadata.read().get(keyspace) { return Ok(ks .iter() - .map(|(k, (ts, tombstone))| (*k, *ts, *tombstone)) + .map(|(k, (ts, tombstone))| (k.clone(), *ts, *tombstone)) .collect::>() .into_iter()); }; @@ -343,12 +343,12 @@ impl Storage for MemStore { .entry(keyspace.to_string()) .and_modify(|entries| { for doc in documents.clone() { - entries.insert(doc.id(), doc); + entries.insert(doc.id().to_vec(), doc); } }) .or_insert_with(|| { HashMap::from_iter( - documents.clone().into_iter().map(|doc| (doc.id(), doc)), + documents.clone().into_iter().map(|doc| (doc.id().to_vec(), doc)), ) }); self.metadata @@ -356,14 +356,14 @@ impl Storage for MemStore { .entry(keyspace.to_string()) .and_modify(|entries| { for doc in documents.clone() { - entries.insert(doc.id(), (doc.last_updated(), false)); + entries.insert(doc.id().to_vec(), (doc.last_updated(), false)); } }) .or_insert_with(|| { HashMap::from_iter( documents .into_iter() - .map(|doc| (doc.id(), (doc.last_updated(), false))), + .map(|doc| (doc.id().to_vec(), (doc.last_updated(), false))), ) }); diff --git a/datacake-eventual-consistency/src/typed_handle.rs b/datacake-eventual-consistency/src/typed_handle.rs new file mode 100644 index 0000000..2c56059 --- /dev/null +++ b/datacake-eventual-consistency/src/typed_handle.rs @@ -0,0 +1,223 @@ +//! Type-safe keyspace handles using the `DatacakeKey` trait. +//! +//! This module provides `TypedKeyspaceHandle` which enforces compile-time +//! type safety for keyspace operations while maintaining runtime validation to +//! prevent mixing key types for the same keyspace. + +use std::marker::PhantomData; + +use datacake_crdt::DatacakeKey; +use datacake_node::Consistency; + +use crate::core::Document; +use crate::error::StoreError; +use crate::storage::Storage; +use crate::ReplicatedKeyspaceHandle; + +/// A type-safe wrapper around a keyspace handle that enforces compile-time +/// type safety for key operations. +/// +/// This handle converts keys of type `K` to `Vec` internally using the +/// `DatacakeKey` trait, providing a more ergonomic and type-safe API. +/// +/// # Example +/// +/// ```rust,no_run +/// use datacake_crdt::DatacakeKey; +/// use datacake_eventual_consistency::TypedKeyspaceHandle; +/// use datacake_node::Consistency; +/// +/// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { +/// // Use String keys directly +/// handle.put("user_123".to_string(), b"user data".to_vec(), Consistency::One).await?; +/// let doc = handle.get("user_123".to_string()).await?; +/// # Ok(()) +/// # } +/// ``` +pub struct TypedKeyspaceHandle +where + K: DatacakeKey, + S: Storage, +{ + inner: ReplicatedKeyspaceHandle, + _phantom: PhantomData, +} + +impl Clone for TypedKeyspaceHandle +where + K: DatacakeKey, + S: Storage, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _phantom: PhantomData, + } + } +} + +impl TypedKeyspaceHandle +where + K: DatacakeKey, + S: Storage, +{ + /// Creates a new typed keyspace handle from an untyped handle. + /// + /// This is an internal constructor used by the store to create typed handles. + pub(crate) fn new(inner: ReplicatedKeyspaceHandle) -> Self { + Self { + inner, + _phantom: PhantomData, + } + } + + /// Retrieves a document from the underlying storage. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// let doc = handle.get("user_123".to_string()).await?; + /// if let Some(doc) = doc { + /// println!("Found document: {:?}", doc.data()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn get(&self, key: K) -> Result, S::Error> { + let key_bytes = key.to_bytes(); + self.inner.get(key_bytes).await + } + + /// Retrieves a set of documents from the underlying storage. + /// + /// If a document does not exist with the given ID, it is simply not part + /// of the returned iterator. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// let keys = vec!["user_1".to_string(), "user_2".to_string(), "user_3".to_string()]; + /// let docs = handle.get_many(keys).await?; + /// for doc in docs { + /// println!("Document: {:?}", doc); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn get_many(&self, keys: I) -> Result + where + I: IntoIterator + Send, + I::IntoIter: Send, + { + let key_bytes = keys.into_iter().map(|k| k.to_bytes()); + self.inner.get_many(key_bytes).await + } + + /// Insert or update a single document into the datastore. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # use datacake_node::Consistency; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// handle.put( + /// "user_123".to_string(), + /// b"user data".to_vec(), + /// Consistency::All + /// ).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn put( + &self, + key: K, + data: Vec, + consistency: Consistency, + ) -> Result<(), StoreError> { + let key_bytes = key.to_bytes(); + self.inner.put(key_bytes, data, consistency).await + } + + /// Insert or update multiple documents into the datastore at once. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # use datacake_node::Consistency; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// let documents = vec![ + /// ("user_1".to_string(), b"data1".to_vec()), + /// ("user_2".to_string(), b"data2".to_vec()), + /// ]; + /// handle.put_many(documents, Consistency::All).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn put_many( + &self, + documents: I, + consistency: Consistency, + ) -> Result<(), StoreError> + where + I: IntoIterator)> + Send, + I::IntoIter: Send, + { + let docs = documents + .into_iter() + .map(|(k, v)| (k.to_bytes(), v)); + self.inner.put_many(docs, consistency).await + } + + /// Delete a document from the datastore with a given key. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # use datacake_node::Consistency; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// handle.del("user_123".to_string(), Consistency::All).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn del( + &self, + key: K, + consistency: Consistency, + ) -> Result<(), StoreError> { + let key_bytes = key.to_bytes(); + self.inner.del(key_bytes, consistency).await + } + + /// Delete multiple documents from the datastore from the set of keys. + /// + /// # Example + /// + /// ```rust,no_run + /// # use datacake_eventual_consistency::TypedKeyspaceHandle; + /// # use datacake_node::Consistency; + /// # async fn example(handle: TypedKeyspaceHandle) -> Result<(), Box> { + /// let keys = vec!["user_1".to_string(), "user_2".to_string()]; + /// handle.del_many(keys, Consistency::All).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn del_many( + &self, + keys: I, + consistency: Consistency, + ) -> Result<(), StoreError> + where + I: IntoIterator + Send, + I::IntoIter: Send, + { + let key_bytes = keys.into_iter().map(|k| k.to_bytes()); + self.inner.del_many(key_bytes, consistency).await + } +} diff --git a/datacake-eventual-consistency/tests/basic_connect.rs b/datacake-eventual-consistency/tests/basic_connect.rs index 639a2b5..ec72f76 100644 --- a/datacake-eventual-consistency/tests/basic_connect.rs +++ b/datacake-eventual-consistency/tests/basic_connect.rs @@ -1,5 +1,10 @@ use std::time::Duration; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + use datacake_eventual_consistency::test_utils::MemStore; use datacake_eventual_consistency::EventuallyConsistentStoreExtension; use datacake_node::{ConnectionConfig, DCAwareSelector, DatacakeNodeBuilder}; diff --git a/datacake-eventual-consistency/tests/dynamic_membership.rs b/datacake-eventual-consistency/tests/dynamic_membership.rs index faf0e0f..fdac330 100644 --- a/datacake-eventual-consistency/tests/dynamic_membership.rs +++ b/datacake-eventual-consistency/tests/dynamic_membership.rs @@ -1,5 +1,10 @@ use std::time::Duration; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + use datacake_eventual_consistency::test_utils::MemStore; use datacake_eventual_consistency::EventuallyConsistentStoreExtension; use datacake_node::{ @@ -58,42 +63,42 @@ pub async fn test_member_join() -> anyhow::Result<()> { let node_2_handle = store_2.handle_with_keyspace("my-keyspace"); node_1_handle - .put(1, b"Hello, world from node-1".to_vec(), Consistency::All) + .put(key(1), b"Hello, world from node-1".to_vec(), Consistency::All) .await .expect("Put value."); node_2_handle - .put(2, b"Hello, world from node-2".to_vec(), Consistency::All) + .put(key(2), b"Hello, world from node-2".to_vec(), Consistency::All) .await .expect("Put value."); let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-1"); let doc = node_1_handle - .get(2) + .get(2_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 2); + assert_eq!(doc.id(), &key(2)); assert_eq!(doc.data(), b"Hello, world from node-2"); let doc = node_2_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-1"); let doc = node_2_handle - .get(2) + .get(2_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 2); + assert_eq!(doc.id(), &key(2)); assert_eq!(doc.data(), b"Hello, world from node-2"); // Node-3 joins the cluster. @@ -110,13 +115,13 @@ pub async fn test_member_join() -> anyhow::Result<()> { let node_3_handle = store_3.handle_with_keyspace("my-keyspace"); node_3_handle - .put(3, b"Hello, world from node-3".to_vec(), Consistency::All) + .put(key(3), b"Hello, world from node-3".to_vec(), Consistency::All) .await .expect("Put value."); - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); - let doc = node_3_handle.get(2).await.expect("Get value."); + let doc = node_3_handle.get(2_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); node_3 @@ -128,33 +133,33 @@ pub async fn test_member_join() -> anyhow::Result<()> { tokio::time::sleep(Duration::from_secs(10)).await; let doc = node_3_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-1"); let doc = node_3_handle - .get(2) + .get(2_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 2); + assert_eq!(doc.id(), &key(2)); assert_eq!(doc.data(), b"Hello, world from node-2"); let doc = node_1_handle - .get(3) + .get(3_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 3); + assert_eq!(doc.id(), &key(3)); assert_eq!(doc.data(), b"Hello, world from node-3"); let doc = node_2_handle - .get(3) + .get(3_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 3); + assert_eq!(doc.id(), &key(3)); assert_eq!(doc.data(), b"Hello, world from node-3"); Ok(()) diff --git a/datacake-eventual-consistency/tests/multi_node_cluster.rs b/datacake-eventual-consistency/tests/multi_node_cluster.rs index 7cf7c8c..3f9c300 100644 --- a/datacake-eventual-consistency/tests/multi_node_cluster.rs +++ b/datacake-eventual-consistency/tests/multi_node_cluster.rs @@ -1,5 +1,10 @@ use std::time::Duration; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + use datacake_eventual_consistency::test_utils::MemStore; use datacake_eventual_consistency::EventuallyConsistentStoreExtension; use datacake_node::{ @@ -31,68 +36,68 @@ async fn test_consistency_all() -> anyhow::Result<()> { let node_3_handle = store_3.handle_with_keyspace("my-keyspace"); // Test reading - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // Test writing node_1_handle - .put(1, b"Hello, world".to_vec(), Consistency::All) + .put(key(1), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); // Node 1 should have the value as it's just written locally. let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); // Nodes 2 and 3 should also have the value immediately due to the consistency level. let doc = node_2_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); let doc = node_3_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); // Delete a key from the cluster node_3_handle - .del(1, Consistency::All) + .del(key(1), Consistency::All) .await .expect("Del value."); // Node 3 should have the value as it's just written locally. - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // Nodes 2 and 1 should also have the value immediately due to the consistency level. - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); // Delete a non-existent key from the cluster node_3_handle - .del(1, Consistency::All) + .del(key(1), Consistency::All) .await .expect("Del value."); // All of the nodes should register the delete. - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); node_1.shutdown().await; @@ -123,28 +128,28 @@ async fn test_consistency_none() -> anyhow::Result<()> { let node_3_handle = store_3.handle_with_keyspace("my-keyspace"); // Test reading - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // Test writing node_1_handle - .put(1, b"Hello, world".to_vec(), Consistency::None) + .put(key(1), b"Hello, world".to_vec(), Consistency::None) .await .expect("Put value."); // Node 1 should have the value as it's just written locally. let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); // Nodes 2 and 3 will not have the value yet as syncing has not taken place. - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // 10 seconds should be enough for this test to propagate state without becoming flaky. @@ -152,50 +157,50 @@ async fn test_consistency_none() -> anyhow::Result<()> { // Nodes 2 and 3 should now see the updated value. let doc = node_2_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); let doc = node_3_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); // Delete a key from the cluster node_3_handle - .del(1, Consistency::None) + .del(key(1), Consistency::None) .await .expect("Del value."); // Node 3 should have the value as it's just written locally. - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); tokio::time::sleep(Duration::from_secs(10)).await; // Nodes should be caught up now. - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); // Delete a non-existent key from the cluster node_3_handle - .del(1, Consistency::None) + .del(key(1), Consistency::None) .await .expect("Del value."); // All of the nodes should register the delete. - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none()); node_1.shutdown().await; @@ -228,73 +233,73 @@ async fn test_async_operations() -> anyhow::Result<()> { // These operations all happen at the exact same time. But they will always be applied in the // same deterministic order. So we know node-3 will win. node_1_handle - .put(1, b"Hello, world from node-1".to_vec(), Consistency::None) + .put(key(1), b"Hello, world from node-1".to_vec(), Consistency::None) .await .expect("Put value."); tokio::time::sleep(Duration::from_millis(2)).await; node_2_handle - .put(1, b"Hello, world from node-2".to_vec(), Consistency::None) + .put(key(1), b"Hello, world from node-2".to_vec(), Consistency::None) .await .expect("Put value."); tokio::time::sleep(Duration::from_millis(2)).await; node_3_handle - .put(1, b"Hello, world from node-3".to_vec(), Consistency::None) + .put(key(1), b"Hello, world from node-3".to_vec(), Consistency::None) .await .expect("Put value."); // *sigh* no consistency in sight! - This is because we haven't given any time to sync yet. let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-1"); let doc = node_2_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-2"); let doc = node_3_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-3"); tokio::time::sleep(Duration::from_secs(10)).await; // Man I love CRDTs, look at how easy this was! They're all the same now. let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-3"); // TODO: This fails if the logical clock isn't correct?? let doc = node_2_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-3"); let doc = node_3_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-3"); // This goes for all operations. // Node 2 will win, even though they're technically happening at the exact same time. node_1_handle .put( - 1, + key(1), b"Hello, world from node-1 but updated".to_vec(), Consistency::None, ) @@ -302,31 +307,31 @@ async fn test_async_operations() -> anyhow::Result<()> { .expect("Put value."); tokio::time::sleep(Duration::from_millis(2)).await; node_2_handle - .del(1, Consistency::None) + .del(key(1), Consistency::None) .await .expect("Delete value."); // Node 1 has only seen it's put so far, so it assumes it's correct. let doc = node_1_handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world from node-1 but updated"); // Node 2 has only seen it's delete so far, so it assumes it's correct. - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "Document should be deleted."); tokio::time::sleep(Duration::from_secs(10)).await; // And now everything is consistent. - let doc = node_1_handle.get(1).await.expect("Get value."); + let doc = node_1_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "Document should be deleted."); - let doc = node_2_handle.get(1).await.expect("Get value."); + let doc = node_2_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "Document should be deleted."); - let doc = node_3_handle.get(1).await.expect("Get value."); + let doc = node_3_handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "Document should be deleted."); node_1.shutdown().await; diff --git a/datacake-eventual-consistency/tests/multiple_keyspace.rs b/datacake-eventual-consistency/tests/multiple_keyspace.rs index 036a487..51d13ee 100644 --- a/datacake-eventual-consistency/tests/multiple_keyspace.rs +++ b/datacake-eventual-consistency/tests/multiple_keyspace.rs @@ -13,6 +13,11 @@ static KEYSPACE_1: &str = "my-first-keyspace"; static KEYSPACE_2: &str = "my-second-keyspace"; static KEYSPACE_3: &str = "my-third-keyspace"; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + #[tokio::test] async fn test_single_node() -> anyhow::Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -33,7 +38,7 @@ async fn test_single_node() -> anyhow::Result<()> { handle .put( KEYSPACE_1, - 1, + key(1), b"Hello, world! From keyspace 1.".to_vec(), Consistency::All, ) @@ -41,25 +46,25 @@ async fn test_single_node() -> anyhow::Result<()> { .expect("Put doc."); handle - .get(KEYSPACE_1, 1) + .get(KEYSPACE_1, key(1)) .await .expect("Get doc.") .expect("Get document just stored."); - let doc = handle.get(KEYSPACE_2, 1).await.expect("Get doc."); + let doc = handle.get(KEYSPACE_2, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = handle.get(KEYSPACE_3, 1).await.expect("Get doc."); + let doc = handle.get(KEYSPACE_3, key(1)).await.expect("Get doc."); assert!(doc.is_none()); handle - .del(KEYSPACE_1, 1, Consistency::All) + .del(KEYSPACE_1, key(1), Consistency::All) .await .expect("Put doc."); - let doc = handle.get(KEYSPACE_1, 1).await.expect("Get doc."); + let doc = handle.get(KEYSPACE_1, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = handle.get(KEYSPACE_2, 1).await.expect("Get doc."); + let doc = handle.get(KEYSPACE_2, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = handle.get(KEYSPACE_3, 1).await.expect("Get doc."); + let doc = handle.get(KEYSPACE_3, key(1)).await.expect("Get doc."); assert!(doc.is_none()); Ok(()) @@ -128,7 +133,7 @@ async fn test_multi_node() -> anyhow::Result<()> { node_1_handle .put( KEYSPACE_1, - 1, + key(1), b"Hello, world! From keyspace 1.".to_vec(), Consistency::All, ) @@ -136,25 +141,25 @@ async fn test_multi_node() -> anyhow::Result<()> { .expect("Put doc."); node_2_handle - .get(KEYSPACE_1, 1) + .get(KEYSPACE_1, key(1)) .await .expect("Get doc.") .expect("Get document just stored."); - let doc = node_3_handle.get(KEYSPACE_2, 1).await.expect("Get doc."); + let doc = node_3_handle.get(KEYSPACE_2, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = node_1_handle.get(KEYSPACE_3, 1).await.expect("Get doc."); + let doc = node_1_handle.get(KEYSPACE_3, key(1)).await.expect("Get doc."); assert!(doc.is_none()); node_2_handle - .del(KEYSPACE_1, 1, Consistency::All) + .del(KEYSPACE_1, key(1), Consistency::All) .await .expect("Put doc."); - let doc = node_2_handle.get(KEYSPACE_1, 1).await.expect("Get doc."); + let doc = node_2_handle.get(KEYSPACE_1, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = node_3_handle.get(KEYSPACE_2, 1).await.expect("Get doc."); + let doc = node_3_handle.get(KEYSPACE_2, key(1)).await.expect("Get doc."); assert!(doc.is_none()); - let doc = node_3_handle.get(KEYSPACE_3, 1).await.expect("Get doc."); + let doc = node_3_handle.get(KEYSPACE_3, key(1)).await.expect("Get doc."); assert!(doc.is_none()); Ok(()) @@ -180,7 +185,7 @@ async fn test_keyspace_list() -> anyhow::Result<()> { handle .put( KEYSPACE_1, - 1, + key(1), b"Hello, world! From keyspace 1.".to_vec(), Consistency::All, ) @@ -194,7 +199,7 @@ async fn test_keyspace_list() -> anyhow::Result<()> { handle .put( KEYSPACE_2, - 1, + key(1), b"Hello, world! From keyspace 2.".to_vec(), Consistency::All, ) @@ -208,7 +213,7 @@ async fn test_keyspace_list() -> anyhow::Result<()> { handle .put( KEYSPACE_3, - 1, + key(1), b"Hello, world! From keyspace 3.".to_vec(), Consistency::All, ) @@ -246,7 +251,7 @@ async fn test_iter_metadata() -> anyhow::Result<()> { handle .put( KEYSPACE_1, - 1, + key(1), b"Hello, world! From keyspace 1. Key 1".to_vec(), Consistency::All, ) @@ -255,7 +260,7 @@ async fn test_iter_metadata() -> anyhow::Result<()> { handle .put( KEYSPACE_1, - 2, + key(2), b"Hello, world! From keyspace 1. Key 2".to_vec(), Consistency::All, ) @@ -264,7 +269,7 @@ async fn test_iter_metadata() -> anyhow::Result<()> { handle .put( KEYSPACE_1, - 3, + key(3), b"Hello, world! From keyspace 1. Key 3".to_vec(), Consistency::All, ) @@ -273,7 +278,7 @@ async fn test_iter_metadata() -> anyhow::Result<()> { handle .put( KEYSPACE_2, - 100, + key(100), b"Hello, world! From keyspace 2. Key 100".to_vec(), Consistency::All, ) @@ -282,27 +287,27 @@ async fn test_iter_metadata() -> anyhow::Result<()> { handle .put( KEYSPACE_2, - 99, + key(99), b"Hello, world! From keyspace 2. Key 99".to_vec(), Consistency::All, ) .await .expect("Put doc."); - let keys: Vec = handle + let keys: Vec> = handle .iter_metadata(KEYSPACE_1) .await? .map(|entry| entry.0) .collect(); - let result: Vec = vec![1, 2, 3]; + let result: Vec> = vec![key(1), key(2), key(3)]; assert!(keys.iter().all(|item| result.contains(item))); - let keys: Vec = handle + let keys: Vec> = handle .iter_metadata(KEYSPACE_2) .await? .map(|entry| entry.0) .collect(); - let result: Vec = vec![100, 99]; + let result: Vec> = vec![key(100), key(99)]; assert!(keys.iter().all(|item| result.contains(item))); Ok(()) diff --git a/datacake-eventual-consistency/tests/single_node_cluster.rs b/datacake-eventual-consistency/tests/single_node_cluster.rs index c9a64c9..e91beeb 100644 --- a/datacake-eventual-consistency/tests/single_node_cluster.rs +++ b/datacake-eventual-consistency/tests/single_node_cluster.rs @@ -1,4 +1,9 @@ use datacake_eventual_consistency::test_utils::MemStore; + +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} use datacake_eventual_consistency::{ EventuallyConsistentStore, EventuallyConsistentStoreExtension, @@ -20,35 +25,35 @@ async fn test_single_node_cluster() -> anyhow::Result<()> { let handle = store.handle(); // Test reading - let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // Test writing handle - .put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All) + .put(KEYSPACE, 1_u64.to_le_bytes().to_vec(), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); let doc = handle - .get(KEYSPACE, 1) + .get(KEYSPACE, key(1)) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); handle - .del(KEYSPACE, 1, Consistency::All) + .del(KEYSPACE, 1_u64.to_le_bytes().to_vec(), Consistency::All) .await .expect("Del value."); - let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); handle - .del(KEYSPACE, 2, Consistency::All) + .del(KEYSPACE, 2_u64.to_le_bytes().to_vec(), Consistency::All) .await .expect("Del value which doesnt exist locally."); - let doc = handle.get(KEYSPACE, 2).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(2)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); Ok(()) @@ -62,32 +67,32 @@ async fn test_single_node_cluster_with_keyspace_handle() -> anyhow::Result<()> { let handle = store.handle_with_keyspace(KEYSPACE); // Test reading - let doc = handle.get(1).await.expect("Get value."); + let doc = handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); // Test writing handle - .put(1, b"Hello, world".to_vec(), Consistency::All) + .put(key(1), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); let doc = handle - .get(1) + .get(1_u64.to_le_bytes().to_vec()) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); - handle.del(1, Consistency::All).await.expect("Del value."); - let doc = handle.get(1).await.expect("Get value."); + handle.del(key(1), Consistency::All).await.expect("Del value."); + let doc = handle.get(1_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); handle - .del(2, Consistency::All) + .del(key(2), Consistency::All) .await .expect("Del value which doesnt exist locally."); - let doc = handle.get(2).await.expect("Get value."); + let doc = handle.get(2_u64.to_le_bytes().to_vec()).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); Ok(()) @@ -102,7 +107,7 @@ async fn test_single_node_cluster_bulk_op() -> anyhow::Result<()> { // Test reading let num_docs = handle - .get_many(KEYSPACE, [1]) + .get_many(KEYSPACE, [key(1)]) .await .expect("Get value.") .count(); @@ -110,35 +115,35 @@ async fn test_single_node_cluster_bulk_op() -> anyhow::Result<()> { // Test writing handle - .put_many(KEYSPACE, [(1, b"Hello, world".to_vec())], Consistency::All) + .put_many(KEYSPACE, [(key(1), b"Hello, world".to_vec())], Consistency::All) .await .expect("Put value."); let docs = handle - .get_many(KEYSPACE, [1]) + .get_many(KEYSPACE, [key(1)]) .await .expect("Get value.") .collect::>(); - assert_eq!(docs[0].id(), 1); + assert_eq!(docs[0].id(), &key(1)); assert_eq!(docs[0].data(), b"Hello, world"); handle - .del_many(KEYSPACE, [1], Consistency::All) + .del_many(KEYSPACE, [key(1)], Consistency::All) .await .expect("Del value."); let num_docs = handle - .get_many(KEYSPACE, [1]) + .get_many(KEYSPACE, [key(1)]) .await .expect("Get value.") .count(); assert_eq!(num_docs, 0, "No document should not exist!"); handle - .del_many(KEYSPACE, [2, 3, 1, 5], Consistency::All) + .del_many(KEYSPACE, [key(2), key(3), key(1), key(5)], Consistency::All) .await .expect("Del value which doesnt exist locally."); let num_docs = handle - .get_many(KEYSPACE, [2, 3, 5, 1]) + .get_many(KEYSPACE, [key(2), key(3), key(5), key(1)]) .await .expect("Get value.") .count(); @@ -155,36 +160,36 @@ async fn test_single_node_cluster_bulk_op_with_keyspace_handle() -> anyhow::Resu let handle = store.handle_with_keyspace(KEYSPACE); // Test reading - let num_docs = handle.get_many([1]).await.expect("Get value.").count(); + let num_docs = handle.get_many([key(1)]).await.expect("Get value.").count(); assert_eq!(num_docs, 0, "No document should not exist!"); // Test writing handle - .put_many([(1, b"Hello, world".to_vec())], Consistency::All) + .put_many([(key(1), b"Hello, world".to_vec())], Consistency::All) .await .expect("Put value."); let docs = handle - .get_many([1]) + .get_many([key(1)]) .await .expect("Get value.") .collect::>(); - assert_eq!(docs[0].id(), 1); + assert_eq!(docs[0].id(), &key(1)); assert_eq!(docs[0].data(), b"Hello, world"); handle - .del_many([1], Consistency::All) + .del_many([key(1)], Consistency::All) .await .expect("Del value."); - let num_docs = handle.get_many([1]).await.expect("Get value.").count(); + let num_docs = handle.get_many([key(1)]).await.expect("Get value.").count(); assert_eq!(num_docs, 0, "No document should not exist!"); handle - .del_many([2, 3, 1, 5], Consistency::All) + .del_many([key(2), key(3), key(1), key(5)], Consistency::All) .await .expect("Del value which doesnt exist locally."); let num_docs = handle - .get_many([2, 3, 5, 1]) + .get_many([key(2), key(3), key(5), key(1)]) .await .expect("Get value.") .count(); diff --git a/datacake-eventual-consistency/tests/typed_keyspaces.rs b/datacake-eventual-consistency/tests/typed_keyspaces.rs new file mode 100644 index 0000000..f0e2e13 --- /dev/null +++ b/datacake-eventual-consistency/tests/typed_keyspaces.rs @@ -0,0 +1,255 @@ +//! Integration tests for typed keyspace handles. + +use datacake_eventual_consistency::test_utils::MemStore; +use datacake_eventual_consistency::{ + EventuallyConsistentStoreExtension, + TypeMismatchError, +}; +use datacake_node::{ + ConnectionConfig, + Consistency, + DCAwareSelector, + DatacakeNodeBuilder, +}; + +static KEYSPACE_USERS: &str = "users"; +static KEYSPACE_COUNTERS: &str = "counters"; +static KEYSPACE_SESSIONS: &str = "sessions"; + +#[tokio::test] +async fn test_typed_handle_with_string_keys() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + let users = store.typed_handle::(KEYSPACE_USERS)?; + + // Test put + users + .put("user_123".to_string(), b"Alice".to_vec(), Consistency::All) + .await?; + + // Test get + let doc = users.get("user_123".to_string()).await?; + assert!(doc.is_some()); + let doc = doc.unwrap(); + assert_eq!(doc.data(), b"Alice"); + + // Test update + users + .put("user_123".to_string(), b"Alice Smith".to_vec(), Consistency::All) + .await?; + + let doc = users.get("user_123".to_string()).await?; + assert!(doc.is_some()); + assert_eq!(doc.unwrap().data(), b"Alice Smith"); + + // Test delete + users.del("user_123".to_string(), Consistency::All).await?; + + let doc = users.get("user_123".to_string()).await?; + assert!(doc.is_none()); + + node.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn test_typed_handle_with_u64_keys() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + let counters = store.typed_handle::(KEYSPACE_COUNTERS)?; + + // Test put + counters.put(42, b"counter_value".to_vec(), Consistency::All).await?; + + // Test get + let doc = counters.get(42).await?; + assert!(doc.is_some()); + let doc = doc.unwrap(); + assert_eq!(doc.data(), b"counter_value"); + + // Test multi_put + let documents = vec![ + (1, b"one".to_vec()), + (2, b"two".to_vec()), + (3, b"three".to_vec()), + ]; + counters.put_many(documents, Consistency::All).await?; + + // Test get_many + let docs: Vec<_> = counters.get_many(vec![1, 2, 3]).await?.collect(); + assert_eq!(docs.len(), 3); + + // Test del_many + counters.del_many(vec![1, 2, 3], Consistency::All).await?; + + let doc = counters.get(1).await?; + assert!(doc.is_none()); + + node.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn test_type_mismatch_error() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + // First access with String + let _users = store.typed_handle::(KEYSPACE_USERS)?; + + // Try to access with u64 should fail + let result = store.typed_handle::(KEYSPACE_USERS); + assert!(result.is_err()); + + match result { + Err(TypeMismatchError::KeyTypeMismatch { keyspace, expected, actual }) => { + assert_eq!(keyspace, KEYSPACE_USERS); + assert!(expected.contains("String")); + assert!(actual.contains("u64")); + } + Ok(_) => panic!("Expected type mismatch error but got Ok"), + } + + node.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn test_mixed_keyspace_types() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + // Create handles with different key types for different keyspaces + let users = store.typed_handle::(KEYSPACE_USERS)?; + let counters = store.typed_handle::(KEYSPACE_COUNTERS)?; + let sessions = store.typed_handle::(KEYSPACE_SESSIONS)?; + + // Use all handles + users.put("user_1".to_string(), b"Alice".to_vec(), Consistency::All).await?; + counters.put(42, b"counter".to_vec(), Consistency::All).await?; + sessions.put("session_abc".to_string(), b"data".to_vec(), Consistency::All).await?; + + // Verify each can be accessed + assert!(users.get("user_1".to_string()).await?.is_some()); + assert!(counters.get(42).await?.is_some()); + assert!(sessions.get("session_abc".to_string()).await?.is_some()); + + // Verify type safety is maintained + let _users2 = store.typed_handle::(KEYSPACE_USERS)?; // OK - same type + assert!(store.typed_handle::(KEYSPACE_USERS).is_err()); // Error - different type + + node.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn test_typed_handle_from_replicated_store_handle() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + // Get the replicated store handle + let handle = store.handle(); + + // Create typed handles from the replicated handle + let users = handle.typed_keyspace::(KEYSPACE_USERS)?; + let counters = handle.typed_keyspace::(KEYSPACE_COUNTERS)?; + + // Use the handles + users.put("user_1".to_string(), b"Alice".to_vec(), Consistency::All).await?; + counters.put(1, b"one".to_vec(), Consistency::All).await?; + + // Verify + assert!(users.get("user_1".to_string()).await?.is_some()); + assert!(counters.get(1).await?.is_some()); + + node.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn test_composite_key_type() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let store = MemStore::default(); + let addr = test_helper::get_unused_addr(); + let connection_cfg = ConnectionConfig::new(addr, addr, Vec::::new()); + + let node = DatacakeNodeBuilder::::new(1, connection_cfg) + .connect() + .await?; + let store = node + .add_extension(EventuallyConsistentStoreExtension::new(store)) + .await?; + + // Create handle with composite key type + let tenants = store.typed_handle::<(String, String)>("tenant_resources")?; + + // Use composite keys (tenant_id, resource_id) + let key1 = ("tenant_abc".to_string(), "resource_123".to_string()); + let key2 = ("tenant_abc".to_string(), "resource_456".to_string()); + let key3 = ("tenant_xyz".to_string(), "resource_123".to_string()); + + tenants.put(key1.clone(), b"data1".to_vec(), Consistency::All).await?; + tenants.put(key2.clone(), b"data2".to_vec(), Consistency::All).await?; + tenants.put(key3.clone(), b"data3".to_vec(), Consistency::All).await?; + + // Verify retrieval + assert!(tenants.get(key1).await?.is_some()); + assert!(tenants.get(key2).await?.is_some()); + assert!(tenants.get(key3).await?.is_some()); + + node.shutdown().await; + Ok(()) +} diff --git a/datacake-lmdb/Cargo.toml b/datacake-lmdb/Cargo.toml index b7bd079..bb5e3cf 100644 --- a/datacake-lmdb/Cargo.toml +++ b/datacake-lmdb/Cargo.toml @@ -13,11 +13,11 @@ readme = "README.md" [dependencies] async-trait = "0.1" -futures = "0.3" +futures = "0.3.31" flume = "0.10" thiserror = "1" -heed = { version = "=0.20.0-alpha.9", default-features = false } +heed = { version = "0.20", default-features = false } tokio = { version = "1", default-features = false, features = ["rt"] } datacake-crdt = { version = "0.5", path = "../datacake-crdt" } diff --git a/datacake-lmdb/README.md b/datacake-lmdb/README.md index 0223eae..7b51dd1 100644 --- a/datacake-lmdb/README.md +++ b/datacake-lmdb/README.md @@ -43,21 +43,21 @@ async fn main() -> Result<()> { let handle = store.handle(); - handle.put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All).await?; - - let doc = handle - .get(KEYSPACE, 1) - .await? - .expect("Document should not be none"); - assert_eq!(doc.id(), 1); - assert_eq!(doc.data(), b"Hello, world"); - - handle.del(KEYSPACE, 1, Consistency::All).await?; - let doc = handle.get(KEYSPACE, 1).await?; - assert!(doc.is_none(), "No document should not exist!"); - - handle.del(KEYSPACE, 2, Consistency::All).await?; - let doc = handle.get(KEYSPACE, 2).await?; + handle.put(KEYSPACE, vec![1], b"Hello, world".to_vec(), Consistency::All).await?; + + let doc = handle + .get(KEYSPACE, vec![1]) + .await? + .expect("Document should not be none"); + assert_eq!(doc.id(), &[1]); + assert_eq!(doc.data(), b"Hello, world"); + + handle.del(KEYSPACE, vec![1], Consistency::All).await?; + let doc = handle.get(KEYSPACE, vec![1]).await?; + assert!(doc.is_none(), "No document should not exist!"); + + handle.del(KEYSPACE, vec![2], Consistency::All).await?; + let doc = handle.get(KEYSPACE, vec![2]).await?; assert!(doc.is_none(), "No document should not exist!"); node.shutdown().await; diff --git a/datacake-lmdb/src/db.rs b/datacake-lmdb/src/db.rs index d3ab8c4..7706c97 100644 --- a/datacake-lmdb/src/db.rs +++ b/datacake-lmdb/src/db.rs @@ -10,8 +10,8 @@ use heed::byteorder::LittleEndian; use heed::types::{Bytes as ByteSlice, Str, Unit, U64}; use heed::{Database, Env, EnvOpenOptions}; -type KvDB = Database, ByteSlice>; -type MetaDB = Database, U64>; +type KvDB = Database; +type MetaDB = Database>; type KeyspaceDB = Database; type DatabaseKeyspace = BTreeMap; type Task = Box; @@ -127,8 +127,8 @@ impl StorageHandle { for pair in meta.iter(&txn)? { let (id, ts) = pair?; - let is_tombstone = kv.get(&txn, &id)?.is_none(); - entries.push((id, HLCTimestamp::from_u64(ts), is_tombstone)); + let is_tombstone = kv.get(&txn, id)?.is_none(); + entries.push((id.to_vec(), HLCTimestamp::from_u64(ts), is_tombstone)); } Ok(entries) @@ -196,13 +196,13 @@ impl StorageHandle { pub(crate) async fn get( &self, keyspace: &str, - key: u64, + key: Key, ) -> heed::Result> { self.submit_task(keyspace, move |env: &Env, kv: &KvDB, meta: &MetaDB| { let txn = env.read_txn()?; if let Some(doc) = kv.get(&txn, &key)? { let ts = meta.get(&txn, &key)?.unwrap(); - Ok(Some(Document::new(key, HLCTimestamp::from_u64(ts), doc))) + Ok(Some(Document::new(key, HLCTimestamp::from_u64(ts), doc.to_vec()))) } else { Ok(None) } @@ -224,7 +224,7 @@ impl StorageHandle { for key in keys { if let Some(doc) = kv.get(&txn, &key)? { let ts = meta.get(&txn, &key)?.unwrap(); - docs.push(Document::new(key, HLCTimestamp::from_u64(ts), doc)); + docs.push(Document::new(key, HLCTimestamp::from_u64(ts), doc.to_vec())); } } @@ -324,10 +324,12 @@ fn setup_disk_handle(path: &Path, tasks: Receiver) -> heed::Result { let _ = std::fs::create_dir_all(path); // Attempt to create the directory. } - let env = EnvOpenOptions::new() - .map_size(DEFAULT_MAP_SIZE) - .max_dbs(MAX_NUM_DBS) - .open(path)?; + let env = unsafe { + EnvOpenOptions::new() + .map_size(DEFAULT_MAP_SIZE) + .max_dbs(MAX_NUM_DBS) + .open(path)? + }; let mut txn = env.write_txn()?; let keyspace_list = env.create_database(&mut txn, Some("datacake-keyspace"))?; @@ -356,6 +358,11 @@ mod tests { use super::*; + // Helper function to convert u64 to Vec for testing + fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() + } + fn get_path() -> PathBuf { let path = temp_dir().join(Uuid::new_v4().to_string()); std::fs::create_dir_all(&path).unwrap(); @@ -375,37 +382,37 @@ mod tests { .await .expect("Database should open OK."); - let doc1 = Document::new(1, HLCTimestamp::from_u64(0), b"Hello".as_ref()); + let doc1 = Document::new(key(1), HLCTimestamp::from_u64(0), b"Hello".as_ref()); handle .put_kv("test", doc1.clone()) .await .expect("Put new doc"); // Test keyspace dont overlap - let doc2 = Document::new(1, HLCTimestamp::from_u64(2), b"Hello 2".as_ref()); + let doc2 = Document::new(key(1), HLCTimestamp::from_u64(2), b"Hello 2".as_ref()); handle .put_kv("test2", doc2.clone()) .await .expect("Put new doc"); - let doc3 = Document::new(1, HLCTimestamp::from_u64(3), b"Hello 3".as_ref()); + let doc3 = Document::new(key(1), HLCTimestamp::from_u64(3), b"Hello 3".as_ref()); handle .put_kv("test3", doc3.clone()) .await .expect("Put new doc"); let fetched_doc_1 = handle - .get("test", 1) + .get("test", key(1)) .await .expect("Get doc") .expect("Doc exists"); let fetched_doc_2 = handle - .get("test2", 1) + .get("test2", key(1)) .await .expect("Get doc") .expect("Doc exists"); let fetched_doc_3 = handle - .get("test3", 1) + .get("test3", key(1)) .await .expect("Get doc") .expect("Doc exists"); @@ -424,9 +431,9 @@ mod tests { .expect("Database should open OK."); let docs = vec![ - Document::new(1, HLCTimestamp::from_u64(0), b"Hello".as_ref()), - Document::new(2, HLCTimestamp::from_u64(0), b"Hello".as_ref()), - Document::new(3, HLCTimestamp::from_u64(0), b"Hello".as_ref()), + Document::new(key(1), HLCTimestamp::from_u64(0), b"Hello".as_ref()), + Document::new(key(2), HLCTimestamp::from_u64(0), b"Hello".as_ref()), + Document::new(key(3), HLCTimestamp::from_u64(0), b"Hello".as_ref()), ]; handle .put_many_kv("test", docs.clone().into_iter()) @@ -434,7 +441,7 @@ mod tests { .expect("Put new docs"); let fetched_docs = handle - .get_many("test", [1, 2, 3].into_iter()) + .get_many("test", [key(1), key(2), key(3)].into_iter()) .await .expect("Get docs"); @@ -447,23 +454,23 @@ mod tests { .await .expect("Database should open OK."); - let doc1 = Document::new(1, HLCTimestamp::from_u64(0), b"Hello".as_ref()); + let doc1 = Document::new(key(1), HLCTimestamp::from_u64(0), b"Hello".as_ref()); handle .put_kv("test", doc1.clone()) .await .expect("Put new doc"); assert!( - handle.get("test", 1).await.expect("Get doc").is_some(), + handle.get("test", key(1)).await.expect("Get doc").is_some(), "Document should exist" ); // Mark it as a tombstone so we shouldn't get it returned anymore. handle - .mark_tombstone("test", doc1.id(), HLCTimestamp::from_u64(1)) + .mark_tombstone("test", doc1.id().to_vec(), HLCTimestamp::from_u64(1)) .await .expect("Put new doc"); assert!( - handle.get("test", 1).await.expect("Get doc").is_none(), + handle.get("test", key(1)).await.expect("Get doc").is_none(), "Document should not exist" ); @@ -473,7 +480,7 @@ mod tests { .await .expect("Put new doc"); assert!( - handle.get("test", 1).await.expect("Get doc").is_some(), + handle.get("test", key(1)).await.expect("Get doc").is_some(), "Document should exist" ); } diff --git a/datacake-lmdb/src/lib.rs b/datacake-lmdb/src/lib.rs index 9d4b502..ee64f27 100644 --- a/datacake-lmdb/src/lib.rs +++ b/datacake-lmdb/src/lib.rs @@ -43,21 +43,21 @@ //! //! let handle = store.handle(); //! -//! handle.put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All).await?; +//! handle.put(KEYSPACE, vec![1], b"Hello, world".to_vec(), Consistency::All).await?; //! //! let doc = handle -//! .get(KEYSPACE, 1) +//! .get(KEYSPACE, vec![1]) //! .await? //! .expect("Document should not be none"); -//! assert_eq!(doc.id(), 1); +//! assert_eq!(doc.id(), &[1]); //! assert_eq!(doc.data(), b"Hello, world"); //! -//! handle.del(KEYSPACE, 1, Consistency::All).await?; -//! let doc = handle.get(KEYSPACE, 1).await?; +//! handle.del(KEYSPACE, vec![1], Consistency::All).await?; +//! let doc = handle.get(KEYSPACE, vec![1]).await?; //! assert!(doc.is_none(), "No document should not exist!"); //! -//! handle.del(KEYSPACE, 2, Consistency::All).await?; -//! let doc = handle.get(KEYSPACE, 2).await?; +//! handle.del(KEYSPACE, vec![2], Consistency::All).await?; +//! let doc = handle.get(KEYSPACE, vec![2]).await?; //! assert!(doc.is_none(), "No document should not exist!"); //! //! node.shutdown().await; diff --git a/datacake-lmdb/tests/basic_cluster.rs b/datacake-lmdb/tests/basic_cluster.rs index a8c0386..70084d9 100644 --- a/datacake-lmdb/tests/basic_cluster.rs +++ b/datacake-lmdb/tests/basic_cluster.rs @@ -13,6 +13,10 @@ use uuid::Uuid; static KEYSPACE: &str = "lmdb-store"; +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + #[tokio::test] async fn test_basic_lmdb_cluster() -> Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -35,30 +39,30 @@ async fn test_basic_lmdb_cluster() -> Result<()> { let handle = store.handle(); handle - .put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All) + .put(KEYSPACE, key(1), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); let doc = handle - .get(KEYSPACE, 1) + .get(KEYSPACE, key(1)) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); handle - .del(KEYSPACE, 1, Consistency::All) + .del(KEYSPACE, key(1), Consistency::All) .await .expect("Del value."); - let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); handle - .del(KEYSPACE, 2, Consistency::All) + .del(KEYSPACE, key(2), Consistency::All) .await .expect("Del value which doesnt exist locally."); - let doc = handle.get(KEYSPACE, 2).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(2)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); node.shutdown().await; diff --git a/datacake-rpc/Cargo.toml b/datacake-rpc/Cargo.toml index 3a3dfea..43033c7 100644 --- a/datacake-rpc/Cargo.toml +++ b/datacake-rpc/Cargo.toml @@ -12,7 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -http = "0.2.8" +http = "1.3" +http-body = "1.0" +http-body-util = "0.1" bytes = "1.3.0" anyhow = "1" async-trait = "0.1.60" @@ -21,7 +23,8 @@ parking_lot = "0.12.1" tracing = "0.1.37" crc32fast = "1.3.2" -hyper = { version = "0.14.23", features = ["full"] } +hyper = { version = "1.0", features = ["full", "server", "http1", "http2"] } +hyper-util = { version = "0.1", features = ["full"] } rkyv = { version = "0.7.42", features = ["strict"] } tokio = { version = "1", default-features = false, features = ["rt"] } diff --git a/datacake-rpc/src/body.rs b/datacake-rpc/src/body.rs index 220e77e..8d8d0bc 100644 --- a/datacake-rpc/src/body.rs +++ b/datacake-rpc/src/body.rs @@ -1,48 +1,80 @@ -use std::ops::{Deref, DerefMut}; use rkyv::{Archive, Serialize}; use crate::rkyv_tooling::DatacakeSerializer; use crate::Status; -/// A wrapper type around the internal [hyper::Body] -pub struct Body(pub(crate) hyper::Body); +use http_body_util::BodyExt; + +/// A wrapper type around different body types +pub enum Body { + Incoming(hyper::body::Incoming), + Full(http_body_util::Full), +} impl Body { - /// Creates a new body. - pub fn new(inner: hyper::Body) -> Self { - Self(inner) + /// Creates a new body from an incoming body. + pub fn new(inner: hyper::body::Incoming) -> Self { + Self::Incoming(inner) + } + + /// Creates a new body from bytes. + pub fn from_bytes(bytes: impl Into) -> Self { + Self::Full(http_body_util::Full::new(bytes.into())) + } + + /// Collects the body into bytes + pub async fn collect(self) -> Result> { + match self { + Body::Incoming(body) => { + let collected = body.collect().await.map_err(|e| Box::new(e) as Box)?; + Ok(collected.to_bytes()) + } + Body::Full(body) => { + let collected = body.collect().await.map_err(|e| Box::new(e) as Box)?; + Ok(collected.to_bytes()) + } + } } - /// Consumes the body returning the inner hyper object. - pub fn into_inner(self) -> hyper::Body { - self.0 + /// Converts to bytes for HTTP responses + pub fn into_http_body(self) -> http_body_util::Full { + match self { + Body::Full(body) => body, + Body::Incoming(_) => { + // For immediate conversion, we return empty body + // In practice, this should be collected first + http_body_util::Full::new(bytes::Bytes::new()) + } + } } } -impl From for Body -where - T: Into, -{ - fn from(value: T) -> Self { - Self(value.into()) +impl From for Body { + fn from(value: hyper::body::Incoming) -> Self { + Self::new(value) } } -impl Deref for Body { - type Target = hyper::Body; +impl From> for Body { + fn from(value: Vec) -> Self { + Self::from_bytes(value) + } +} - fn deref(&self) -> &Self::Target { - &self.0 +impl From for Body { + fn from(value: String) -> Self { + Self::from_bytes(value) } } -impl DerefMut for Body { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +impl From<&'static str> for Body { + fn from(value: &'static str) -> Self { + Self::from_bytes(value) } } + /// The serializer trait converting replies into hyper bodies. /// /// This is a light abstraction to allow users to be able to diff --git a/datacake-rpc/src/client.rs b/datacake-rpc/src/client.rs index c226adc..dfba1f1 100644 --- a/datacake-rpc/src/client.rs +++ b/datacake-rpc/src/client.rs @@ -175,7 +175,7 @@ where #[inline] /// Creates a new RPC context which can customise more of /// the request than the convenience methods, i.e. Headers. - pub fn create_rpc_context(&self) -> RpcContext { + pub fn create_rpc_context(&self) -> RpcContext<'_, Svc> { RpcContext { client: self, headers: HeaderMap::new(), @@ -298,9 +298,9 @@ where return <>::Reply>::from_body(Body::new(body)).await; } - let buffer = crate::utils::to_aligned(body) + let buffer = crate::utils::to_aligned(Body::new(body)) .await - .map_err(|e| Status::internal(e.message()))?; + .map_err(|e| Status::internal(e.to_string()))?; let status = DataView::::using(buffer).map_err(|_| Status::invalid())?; Err(status .deserialize_view() diff --git a/datacake-rpc/src/net/client.rs b/datacake-rpc/src/net/client.rs index 0bebfb2..34ad29c 100644 --- a/datacake-rpc/src/net/client.rs +++ b/datacake-rpc/src/net/client.rs @@ -12,7 +12,7 @@ use crate::request::MessageMetadata; /// A raw client connection which can produce multiplexed streams. pub struct Channel { #[cfg(not(feature = "simulation"))] - connection: hyper::Client, + connection: hyper_util::client::legacy::Client>, #[cfg(feature = "simulation")] connection: LazyClient, @@ -24,12 +24,12 @@ impl Channel { #[cfg(not(feature = "simulation"))] /// Connects to a remote RPC server. pub fn connect(remote_addr: SocketAddr) -> Self { - let mut http = hyper::client::HttpConnector::new(); + let mut http = hyper_util::client::legacy::connect::HttpConnector::new(); http.enforce_http(false); http.set_nodelay(true); http.set_connect_timeout(Some(std::time::Duration::from_secs(2))); - let client = hyper::Client::builder() + let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .http2_keep_alive_while_idle(true) .http2_only(true) .http2_adaptive_window(true) @@ -59,12 +59,19 @@ impl Channel { metadata: MessageMetadata, headers: HeaderMap, body: Body, - ) -> Result, Error> { + ) -> Result, Error> { let uri = format!("http://{}{}", self.remote_addr, metadata.to_uri_path(),); + + // Convert the body to bytes and then to Full + let body_bytes = match body.collect().await { + Ok(bytes) => bytes, + Err(_) => bytes::Bytes::new(), + }; + let mut request = Request::builder() .method(Method::POST) .uri(uri) - .body(body.into_inner()) + .body(http_body_util::Full::new(body_bytes)) .unwrap(); (*request.headers_mut()) = headers; diff --git a/datacake-rpc/src/net/mod.rs b/datacake-rpc/src/net/mod.rs index a4477a2..1afbc34 100644 --- a/datacake-rpc/src/net/mod.rs +++ b/datacake-rpc/src/net/mod.rs @@ -20,4 +20,7 @@ pub enum Error { #[error("Hyper Error: {0}")] /// The operation failed due an error originating in hyper. Hyper(#[from] hyper::Error), + #[error("Hyper Util Error: {0}")] + /// The operation failed due an error originating in hyper-util. + HyperUtil(#[from] hyper_util::client::legacy::Error), } diff --git a/datacake-rpc/src/net/server.rs b/datacake-rpc/src/net/server.rs index 3d45ec1..abcc9f7 100644 --- a/datacake-rpc/src/net/server.rs +++ b/datacake-rpc/src/net/server.rs @@ -4,11 +4,12 @@ use std::net::SocketAddr; use std::time::Duration; use http::{Request, Response, StatusCode}; -use hyper::server::conn::Http; +use hyper::server::conn::http2; use hyper::service::service_fn; use rkyv::AlignedVec; use tokio::sync::oneshot; use tokio::task::JoinHandle; +use tracing::{error, warn}; use crate::body::Body; use crate::server::ServerState; @@ -46,11 +47,10 @@ pub(crate) async fn start_rpc_server( handle_connection(req, state.clone(), remote_addr) }); - let connection = Http::new() - .http2_only(true) - .http2_adaptive_window(true) - .http2_keep_alive_timeout(Duration::from_secs(10)) - .serve_connection(io, handler); + let connection = http2::Builder::new(hyper_util::rt::TokioExecutor::new()) + .adaptive_window(true) + .keep_alive_timeout(Duration::from_secs(10)) + .serve_connection(hyper_util::rt::TokioIo::new(io), handler); if let Err(e) = connection.await { error!(error = ?e, "Error while serving HTTP connection."); @@ -69,14 +69,14 @@ pub(crate) async fn start_rpc_server( /// This accepts new streams being created and spawns concurrent tasks to handle /// them. async fn handle_connection( - req: Request, + req: Request, state: ServerState, remote_addr: SocketAddr, -) -> Result, Infallible> { +) -> Result>, Infallible> { match handle_message(req, state, remote_addr).await { Ok(r) => Ok(r), Err(e) => { - let mut response = Response::new(e.to_string().into()); + let mut response = Response::new(http_body_util::Full::new(bytes::Bytes::from(e.to_string()))); (*response.status_mut()) = StatusCode::INTERNAL_SERVER_ERROR; Ok(response) }, @@ -84,15 +84,20 @@ async fn handle_connection( } async fn handle_message( - req: Request, + req: Request, state: ServerState, remote_addr: SocketAddr, -) -> anyhow::Result> { +) -> anyhow::Result>> { let reply = try_handle_request(req, state, remote_addr).await; match reply { Ok(body) => { - let mut response = Response::new(body.into_inner()); + // Convert the body to bytes for the response + let bytes = match body.collect().await { + Ok(bytes) => bytes, + Err(_) => bytes::Bytes::new(), + }; + let mut response = Response::new(http_body_util::Full::new(bytes)); (*response.status_mut()) = StatusCode::OK; Ok(response) }, @@ -101,7 +106,7 @@ async fn handle_message( } async fn try_handle_request( - req: Request, + req: Request, state: ServerState, remote_addr: SocketAddr, ) -> Result { @@ -118,12 +123,12 @@ async fn try_handle_request( .await } -fn create_bad_request(status: &Status) -> Response { +fn create_bad_request(status: &Status) -> Response> { // This should be infallible. let buffer = crate::rkyv_tooling::to_view_bytes(status).unwrap_or_else(|_| AlignedVec::new()); - let mut response = Response::new(buffer.to_vec().into()); + let mut response = Response::new(http_body_util::Full::new(bytes::Bytes::from(buffer.to_vec()))); (*response.status_mut()) = StatusCode::BAD_REQUEST; response diff --git a/datacake-rpc/src/net/simulation.rs b/datacake-rpc/src/net/simulation.rs index 341d84b..ea69902 100644 --- a/datacake-rpc/src/net/simulation.rs +++ b/datacake-rpc/src/net/simulation.rs @@ -4,8 +4,9 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; -use hyper::client::conn::SendRequest; -use hyper::Body; +use hyper::client::conn::http2::SendRequest; +use http_body_util::Full; +use bytes::Bytes; use tokio::sync::{Mutex, OnceCell}; use tokio::time::timeout; @@ -18,7 +19,7 @@ use crate::net::Error; /// performance. pub struct LazyClient { addr: SocketAddr, - client: Arc>>>, + client: Arc>>>>, } impl LazyClient { @@ -31,7 +32,7 @@ impl LazyClient { } /// Ensures the connection is initialised and ready to handle events. - pub async fn get_or_init(&self) -> Result<&Mutex>, Error> { + pub async fn get_or_init(&self) -> Result<&Mutex>>, Error> { if let Some(existing) = self.client.get() { return Ok(existing); } @@ -48,11 +49,10 @@ impl LazyClient { )) })??; - let (sender, connection) = hyper::client::conn::Builder::new() - .http2_keep_alive_while_idle(true) - .http2_only(true) - .http2_adaptive_window(true) - .handshake(io) + let (sender, connection) = hyper::client::conn::http2::Builder::new(hyper_util::rt::TokioExecutor::new()) + .keep_alive_while_idle(true) + .adaptive_window(true) + .handshake(hyper_util::rt::TokioIo::new(io)) .await?; tokio::spawn(async move { diff --git a/datacake-rpc/src/request.rs b/datacake-rpc/src/request.rs index 6e34945..420fcea 100644 --- a/datacake-rpc/src/request.rs +++ b/datacake-rpc/src/request.rs @@ -40,9 +40,9 @@ where type Content = DataView; async fn from_body(body: Body) -> Result { - let bytes = crate::utils::to_aligned(body.0) + let bytes = crate::utils::to_aligned(body) .await - .map_err(Status::internal)?; + .map_err(|e| Status::internal(e.to_string()))?; DataView::using(bytes).map_err(|_| Status::invalid()) } diff --git a/datacake-rpc/src/utils.rs b/datacake-rpc/src/utils.rs index 3fe5d24..e92b82b 100644 --- a/datacake-rpc/src/utils.rs +++ b/datacake-rpc/src/utils.rs @@ -1,35 +1,11 @@ -use bytes::Buf; -use hyper::body::HttpBody; -use hyper::Body; use rkyv::AlignedVec; +use crate::body::Body; pub async fn to_aligned( - mut body: Body, -) -> Result::Error> { - // If there's only 1 chunk, we can just return Buf::to_bytes() - let first = if let Some(buf) = body.data().await { - buf? - } else { - return Ok(AlignedVec::new()); - }; - - let second = if let Some(buf) = body.data().await { - buf? - } else { - let mut vec = AlignedVec::with_capacity(first.len()); - vec.extend_from_slice(&first); - return Ok(vec); - }; - - // With more than 1 buf, we gotta flatten into a Vec first. - let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize; - let mut vec = AlignedVec::with_capacity(cap); - vec.extend_from_slice(&first); - vec.extend_from_slice(&second); - - while let Some(buf) = body.data().await { - vec.extend_from_slice(&buf?); - } - + body: Body, +) -> Result> { + let bytes = body.collect().await?; + let mut vec = AlignedVec::with_capacity(bytes.len()); + vec.extend_from_slice(&bytes); Ok(vec) } diff --git a/datacake-rpc/tests/stream.rs b/datacake-rpc/tests/stream.rs index c695455..c482e62 100644 --- a/datacake-rpc/tests/stream.rs +++ b/datacake-rpc/tests/stream.rs @@ -59,7 +59,7 @@ async fn test_stream_body() { }; let resp = rpc_client.send(&msg1).await.unwrap(); - let body = hyper::body::to_bytes(resp.into_inner()).await.unwrap(); + let body = resp.collect().await.unwrap(); assert_eq!(msg1.buffer, body.as_ref()); server.shutdown(); diff --git a/datacake-sqlite/Cargo.toml b/datacake-sqlite/Cargo.toml index 73b6087..e863ff1 100644 --- a/datacake-sqlite/Cargo.toml +++ b/datacake-sqlite/Cargo.toml @@ -14,9 +14,9 @@ readme = "README.md" [dependencies] anyhow = "1" async-trait = "0.1.59" -futures = "0.3.25" +futures = "0.3.31" flume = "0.10.14" -rusqlite = "0.28.0" +rusqlite = "0.32" thiserror = "1" tokio = { version = "1", default-features = false, features = ["rt"] } diff --git a/datacake-sqlite/README.md b/datacake-sqlite/README.md index 09d36e2..260b2df 100644 --- a/datacake-sqlite/README.md +++ b/datacake-sqlite/README.md @@ -24,6 +24,11 @@ use datacake_sqlite::SqliteStorage; static KEYSPACE: &str = "sqlite-store"; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + #[tokio::test] async fn test_basic_sqlite_cluster() -> Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -43,30 +48,30 @@ async fn test_basic_sqlite_cluster() -> Result<()> { let handle = store.handle(); handle - .put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All) + .put(KEYSPACE, key(1), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); let doc = handle - .get(KEYSPACE, 1) + .get(KEYSPACE, key(1)) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); handle - .del(KEYSPACE, 1, Consistency::All) + .del(KEYSPACE, key(1), Consistency::All) .await .expect("Del value."); - let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); handle - .del(KEYSPACE, 2, Consistency::All) + .del(KEYSPACE, key(2), Consistency::All) .await .expect("Del value which doesnt exist locally."); - let doc = handle.get(KEYSPACE, 2).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(2)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); node.shutdown().await; diff --git a/datacake-sqlite/src/lib.rs b/datacake-sqlite/src/lib.rs index 5cca9ec..bbc085f 100644 --- a/datacake-sqlite/src/lib.rs +++ b/datacake-sqlite/src/lib.rs @@ -24,6 +24,11 @@ //! //! static KEYSPACE: &str = "sqlite-store"; //! +//! // Helper function to convert u64 to Vec for keys +//! fn key(n: u64) -> Vec { +//! n.to_le_bytes().to_vec() +//! } +//! //! #[tokio::test] //! async fn test_basic_sqlite_cluster() -> Result<()> { //! let _ = tracing_subscriber::fmt::try_init(); @@ -43,30 +48,30 @@ //! let handle = store.handle(); //! //! handle -//! .put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All) +//! .put(KEYSPACE, key(1), b"Hello, world".to_vec(), Consistency::All) //! .await //! .expect("Put value."); //! //! let doc = handle -//! .get(KEYSPACE, 1) +//! .get(KEYSPACE, key(1)) //! .await //! .expect("Get value.") //! .expect("Document should not be none"); -//! assert_eq!(doc.id(), 1); +//! assert_eq!(doc.id(), &key(1)); //! assert_eq!(doc.data(), b"Hello, world"); //! //! handle -//! .del(KEYSPACE, 1, Consistency::All) +//! .del(KEYSPACE, key(1), Consistency::All) //! .await //! .expect("Del value."); -//! let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); +//! let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); //! assert!(doc.is_none(), "No document should not exist!"); //! //! handle -//! .del(KEYSPACE, 2, Consistency::All) +//! .del(KEYSPACE, key(2), Consistency::All) //! .await //! .expect("Del value which doesnt exist locally."); -//! let doc = handle.get(KEYSPACE, 2).await.expect("Get value."); +//! let doc = handle.get(KEYSPACE, key(2)).await.expect("Get value."); //! assert!(doc.is_none(), "No document should not exist!"); //! //! node.shutdown().await; @@ -194,7 +199,7 @@ impl Storage for SqliteStorage { keys: impl Iterator + Send, ) -> Result<(), BulkMutationError> { let params = keys - .map(|doc_id| (keyspace.to_string(), doc_id as i64)) + .map(|doc_id| (keyspace.to_string(), doc_id)) .collect::>(); self.inner .execute_many(queries::DELETE_TOMBSTONE, params) @@ -209,7 +214,7 @@ impl Storage for SqliteStorage { queries::INSERT, ( keyspace.to_string(), - doc.id() as i64, + doc.id().to_vec(), doc.last_updated().to_string(), doc.data().to_vec(), ), @@ -227,7 +232,7 @@ impl Storage for SqliteStorage { .map(|doc| { ( keyspace.to_string(), - doc.id() as i64, + doc.id().to_vec(), doc.last_updated().to_string(), doc.data().to_vec(), ) @@ -249,7 +254,7 @@ impl Storage for SqliteStorage { self.inner .execute( queries::SET_TOMBSTONE, - (keyspace.to_string(), doc_id as i64, timestamp.to_string()), + (keyspace.to_string(), doc_id, timestamp.to_string()), ) .await?; Ok(()) @@ -264,7 +269,7 @@ impl Storage for SqliteStorage { .map(|doc| { ( keyspace.to_string(), - doc.id as i64, + doc.id, doc.last_updated.to_string(), ) }) @@ -285,7 +290,7 @@ impl Storage for SqliteStorage { .inner .fetch_one::<_, models::Doc>( queries::SELECT_DOC, - (keyspace.to_string(), doc_id as i64), + (keyspace.to_string(), doc_id), ) .await?; @@ -346,7 +351,7 @@ mod models { pub struct Doc(pub Document); impl FromRow for Doc { fn from_row(row: &Row) -> rusqlite::Result { - let id = row.get::<_, i64>(0)? as Key; + let id = row.get::<_, Vec>(0)?; let ts = row.get::<_, String>(1)?; let data = row.get::<_, Vec>(2)?; @@ -360,7 +365,7 @@ mod models { pub struct Metadata(pub Key, pub HLCTimestamp, pub bool); impl FromRow for Metadata { fn from_row(row: &Row) -> rusqlite::Result { - let id = row.get::<_, i64>(0)? as Key; + let id = row.get::<_, Vec>(0)?; let ts = row.get::<_, String>(1)?; let is_tombstone = row.get::<_, bool>(2)?; @@ -376,7 +381,7 @@ async fn setup_db(handle: StorageHandle) -> rusqlite::Result<()> { let table = r#" CREATE TABLE IF NOT EXISTS state_entries ( keyspace TEXT, - doc_id BIGINT, + doc_id BLOB, ts TEXT, data BLOB, PRIMARY KEY (keyspace, doc_id) diff --git a/datacake-sqlite/tests/basic_cluster.rs b/datacake-sqlite/tests/basic_cluster.rs index 710d2a4..8c3a81d 100644 --- a/datacake-sqlite/tests/basic_cluster.rs +++ b/datacake-sqlite/tests/basic_cluster.rs @@ -10,6 +10,11 @@ use datacake_sqlite::SqliteStorage; static KEYSPACE: &str = "sqlite-store"; +// Helper function to convert u64 to Vec for keys +fn key(n: u64) -> Vec { + n.to_le_bytes().to_vec() +} + #[tokio::test] async fn test_basic_sqlite_cluster() -> Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -29,30 +34,30 @@ async fn test_basic_sqlite_cluster() -> Result<()> { let handle = store.handle(); handle - .put(KEYSPACE, 1, b"Hello, world".to_vec(), Consistency::All) + .put(KEYSPACE, key(1), b"Hello, world".to_vec(), Consistency::All) .await .expect("Put value."); let doc = handle - .get(KEYSPACE, 1) + .get(KEYSPACE, key(1)) .await .expect("Get value.") .expect("Document should not be none"); - assert_eq!(doc.id(), 1); + assert_eq!(doc.id(), &key(1)); assert_eq!(doc.data(), b"Hello, world"); handle - .del(KEYSPACE, 1, Consistency::All) + .del(KEYSPACE, key(1), Consistency::All) .await .expect("Del value."); - let doc = handle.get(KEYSPACE, 1).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(1)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); handle - .del(KEYSPACE, 2, Consistency::All) + .del(KEYSPACE, key(2), Consistency::All) .await .expect("Del value which doesnt exist locally."); - let doc = handle.get(KEYSPACE, 2).await.expect("Get value."); + let doc = handle.get(KEYSPACE, key(2)).await.expect("Get value."); assert!(doc.is_none(), "No document should not exist!"); node.shutdown().await; diff --git a/examples/replicated-kv/Cargo.toml b/examples/replicated-kv/Cargo.toml index 1388b50..b26baf0 100644 --- a/examples/replicated-kv/Cargo.toml +++ b/examples/replicated-kv/Cargo.toml @@ -18,7 +18,7 @@ serde_json = "1.0.89" serde = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } tokio = { version = "1", features = ["full"] } -rusqlite = { version = "0.28.0", features = ["bundled"] } +rusqlite = { version = "0.32", features = ["bundled"] } datacake = { version = "0.8", path = "../..", features = ["datacake-sqlite"] } [dev-dependencies] diff --git a/examples/replicated-kv/src/main.rs b/examples/replicated-kv/src/main.rs index c2ff1d9..1b9d8dc 100644 --- a/examples/replicated-kv/src/main.rs +++ b/examples/replicated-kv/src/main.rs @@ -112,8 +112,9 @@ async fn get_value( "Getting document!" ); + let key_bytes = params.key.to_le_bytes().to_vec(); let doc = handle - .get(¶ms.keyspace, params.key) + .get(¶ms.keyspace, key_bytes) .await .map_err(|e| { error!(error = ?e, doc_id = params.key, "Failed to fetch doc."); @@ -137,8 +138,9 @@ async fn set_value( "Storing document!" ); + let key_bytes = params.key.to_le_bytes().to_vec(); handle - .put(¶ms.keyspace, params.key, data, Consistency::EachQuorum) + .put(¶ms.keyspace, key_bytes, data, Consistency::EachQuorum) .await .map_err(|e| { error!(error = ?e, doc_id = params.key, "Failed to fetch doc."); diff --git a/examples/replicated-kv/src/storage.rs b/examples/replicated-kv/src/storage.rs index bae6fda..a341ebb 100644 --- a/examples/replicated-kv/src/storage.rs +++ b/examples/replicated-kv/src/storage.rs @@ -40,7 +40,7 @@ impl ShardedStorage { Ok(Self { shards }) } - fn get_shard_id(&self, key: Key) -> usize { + fn get_shard_id(&self, key: &[u8]) -> usize { // This probably shouldn't be crc based but it's just for a demo. let mut hasher = crc32fast::Hasher::new(); key.hash(&mut hasher); @@ -99,7 +99,7 @@ impl Storage for ShardedStorage { shard_blocks.resize_with(self.shards.len(), Vec::new); for key in keys { - let shard_id = self.get_shard_id(key); + let shard_id = self.get_shard_id(&key); shard_blocks[shard_id].push(key); } @@ -111,7 +111,7 @@ impl Storage for ShardedStorage { .await; if let Err(e) = res { - successful_ids.extend(e.successful_doc_ids().iter().copied()); + successful_ids.extend(e.successful_doc_ids().iter().cloned()); error = Some(e.into_inner()); } else { successful_ids.extend(doc_ids); @@ -147,8 +147,9 @@ impl Storage for ShardedStorage { for doc in documents { total_docs += 1; - let shard_id = self.get_shard_id(doc.id()); - doc_id_blocks[shard_id].push(doc.id()); + let doc_id = doc.id().to_vec(); + let shard_id = self.get_shard_id(&doc_id); + doc_id_blocks[shard_id].push(doc_id); shard_blocks[shard_id].push(doc); } @@ -162,7 +163,7 @@ impl Storage for ShardedStorage { .await; if let Err(e) = res { - successful_ids.extend(e.successful_doc_ids().iter().copied()); + successful_ids.extend(e.successful_doc_ids().iter().cloned()); error = Some(e.into_inner()); } else { successful_ids.extend(doc_ids); @@ -182,7 +183,7 @@ impl Storage for ShardedStorage { doc_id: Key, timestamp: HLCTimestamp, ) -> std::result::Result<(), Self::Error> { - let shard_id = self.get_shard_id(doc_id); + let shard_id = self.get_shard_id(&doc_id); self.shards[shard_id] .mark_as_tombstone(keyspace, doc_id, timestamp) .await @@ -201,8 +202,8 @@ impl Storage for ShardedStorage { for doc in documents { total_docs += 1; - let shard_id = self.get_shard_id(doc.id); - doc_id_blocks[shard_id].push(doc.id); + let shard_id = self.get_shard_id(&doc.id); + doc_id_blocks[shard_id].push(doc.id.clone()); shard_blocks[shard_id].push(doc); } @@ -216,7 +217,7 @@ impl Storage for ShardedStorage { .await; if let Err(e) = res { - successful_ids.extend(e.successful_doc_ids().iter().copied()); + successful_ids.extend(e.successful_doc_ids().iter().cloned()); error = Some(e.into_inner()); } else { successful_ids.extend(doc_ids); @@ -235,7 +236,7 @@ impl Storage for ShardedStorage { keyspace: &str, doc_id: Key, ) -> std::result::Result, Self::Error> { - let shard_id = self.get_shard_id(doc_id); + let shard_id = self.get_shard_id(&doc_id); self.shards[shard_id].get(keyspace, doc_id).await } @@ -248,7 +249,7 @@ impl Storage for ShardedStorage { shard_blocks.resize_with(self.shards.len(), Vec::new); for doc_id in doc_ids { - let shard_id = self.get_shard_id(doc_id); + let shard_id = self.get_shard_id(&doc_id); shard_blocks[shard_id].push(doc_id); } diff --git a/src/lib.rs b/src/lib.rs index 004c163..6517ccc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,10 @@ //! use datacake::eventual_consistency::test_utils::MemStore; //! use datacake::eventual_consistency::EventuallyConsistentStoreExtension; //! +//! fn key(n: u64) -> Vec { +//! n.to_le_bytes().to_vec() +//! } +//! //! #[tokio::main] //! async fn main() -> anyhow::Result<()> { //! let addr = "127.0.0.1:8080".parse::().unwrap(); @@ -65,7 +69,7 @@ //! handle //! .put( //! "my-keyspace", -//! 1, +//! key(1), //! b"Hello, world! From keyspace 1.".to_vec(), //! Consistency::All, //! )