Skip to content

Commit a5a193b

Browse files
authored
Extension key namespaces (#21)
1 parent 393a306 commit a5a193b

11 files changed

+449
-33
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

simd-r-drive-extensions/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "simd-r-drive-extensions"
33
authors = ["Jeremy Harris <[email protected]>"]
4-
version = "0.4.0-alpha.4"
4+
version = "0.4.0-alpha.5"
55
edition = "2021"
66
repository = "https://github.com/jzombie/rust-simd-r-drive"
77
description = "Storage extensions for SIMD R Drive."
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// Special marker for explicitly storing `None` values in binary storage.
2+
pub(crate) const OPTION_TOMBSTONE_MARKER: [u8; 2] = [0xFF, 0xFE];
3+
4+
/// # Namespaced Prefixes for Storage Features
5+
///
6+
/// These prefixes are **not** used to differentiate values themselves;
7+
/// SIMD R Drive already handles type and structure differentiation.
8+
///
9+
/// Instead, these prefixes are used to distinguish **storage features** such as:
10+
/// - **Option handling** (explicit tombstones for `None` values)
11+
/// - **TTL-based auto-eviction** (keys prefixed with expiration timestamps)
12+
///
13+
/// By applying **feature-based** prefixes, we ensure that:
14+
/// - Different feature extensions do not naturally conflict.
15+
/// - Per-extensions read/write operations apply the correct logic.
16+
/// - Keys remain distinct even if their raw values are identical.
17+
///
18+
/// This ensures relatively safe, efficient, and collision-free feature separation
19+
/// without interfering with the actual stored values.
20+
macro_rules! namespace_prefix {
21+
($name:expr) => {{
22+
const PREFIX: &[u8] = &{
23+
const LEN: usize = $name.len();
24+
let mut arr = [0u8; LEN + 2]; // Boundary + Name + Boundary
25+
26+
arr[0] = 0xF7; // Start Boundary: Non-standard, forbidden high UTF-8 range
27+
arr[LEN + 1] = 0xFD; // End Boundary: Another high, rarely used byte
28+
29+
let mut i = 0;
30+
while i < LEN {
31+
arr[i + 1] = $name[i];
32+
i += 1;
33+
}
34+
arr
35+
};
36+
PREFIX
37+
}};
38+
}
39+
40+
/// Namespaced extension prefixes
41+
pub(crate) const OPTION_PREFIX: &[u8] = namespace_prefix!(b"option");
42+
pub(crate) const TTL_PREFIX: &[u8] = namespace_prefix!(b"ttl");

simd-r-drive-extensions/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
#[cfg(doctest)]
22
doc_comment::doctest!("../README.md");
33

4+
pub mod utils;
5+
pub use utils::option_serializer::*;
6+
47
mod storage_option_ext;
58
pub use storage_option_ext::*;
69

710
mod storage_cache_ext;
811
pub use storage_cache_ext::*;
12+
13+
mod constants;

simd-r-drive-extensions/src/storage_cache_ext.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
use crate::constants::TTL_PREFIX;
2+
use crate::utils::prefix_key;
13
use serde::de::DeserializeOwned;
24
use serde::Serialize;
35
use simd_r_drive::DataStore;
46
use std::io::{self, ErrorKind};
57
use std::time::{SystemTime, UNIX_EPOCH};
68

7-
/// **Prefix-based TTL storage for cache expiration**
9+
#[cfg(any(test, debug_assertions))]
10+
pub const TEST_TTL_PREFIX: &[u8] = TTL_PREFIX;
11+
12+
/// # Storage Utilities for Handling Auto-Evicting TTL Entries
13+
///
814
/// Stores a timestamp (in seconds) before the actual value.
15+
///
16+
/// Note: Option types are *safely* handled by this without additional serialization
17+
/// as they are stored with the TTL value as well.
918
pub trait StorageCacheExt {
1019
/// Writes a value with a TTL (Time-To-Live).
1120
///
@@ -25,6 +34,7 @@ pub trait StorageCacheExt {
2534

2635
/// Reads a value, checking TTL expiration.
2736
///
37+
/// - **⚠️ Non Zero-Copy Warning**: Requires deserialization.
2838
/// - If the TTL has expired, the key is **automatically evicted**, and `None` is returned.
2939
/// - If the key does not exist, returns `Err(ErrorKind::NotFound)`.
3040
/// - If deserialization fails, returns `Err(ErrorKind::InvalidData)`.
@@ -44,6 +54,8 @@ impl StorageCacheExt for DataStore {
4454
value: &T,
4555
ttl_secs: u64,
4656
) -> io::Result<u64> {
57+
let key = &prefix_key(TTL_PREFIX, key);
58+
4759
let expiration_timestamp = SystemTime::now()
4860
.duration_since(UNIX_EPOCH)
4961
.expect("Time went backwards")
@@ -59,6 +71,8 @@ impl StorageCacheExt for DataStore {
5971
}
6072

6173
fn read_with_ttl<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, io::Error> {
74+
let key = &prefix_key(TTL_PREFIX, key);
75+
6276
match self.read(key) {
6377
Some(entry) => {
6478
let data = entry.as_slice();
@@ -77,7 +91,7 @@ impl StorageCacheExt for DataStore {
7791
.as_secs();
7892

7993
if now >= expiration_timestamp {
80-
self.delete_entry(key).ok(); // Remove expired entry
94+
self.delete_entry(key.as_slice()).ok(); // Remove expired entry
8195
return Ok(None);
8296
}
8397

simd-r-drive-extensions/src/storage_option_ext.rs

+16-25
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
use bincode;
1+
use crate::constants::{OPTION_PREFIX, OPTION_TOMBSTONE_MARKER};
2+
use crate::utils::prefix_key;
3+
use crate::{deserialize_option, serialize_option};
24
use serde::de::DeserializeOwned;
35
use serde::Serialize;
46
use simd_r_drive::DataStore;
57
use std::io::{self, ErrorKind};
68

7-
/// Special marker for explicitly storing `None` values in binary storage.
8-
/// This ensures that `None` is distinguishable from an empty or default value.
9-
const OPTION_TOMBSTONE_MARKER: [u8; 2] = [0xFF, 0xFE];
10-
119
#[cfg(any(test, debug_assertions))]
1210
pub const TEST_OPTION_TOMBSTONE_MARKER: [u8; 2] = OPTION_TOMBSTONE_MARKER;
1311

12+
#[cfg(any(test, debug_assertions))]
13+
pub const TEST_OPTION_PREFIX: &[u8] = OPTION_PREFIX;
14+
1415
/// # Storage Utilities for Handling `Option<T>`
1516
///
1617
/// This trait provides methods to store and retrieve `Option<T>` values
@@ -122,32 +123,22 @@ pub trait StorageOptionExt {
122123

123124
/// Implements `StorageOptionExt` for `DataStore`
124125
impl StorageOptionExt for DataStore {
125-
fn write_option<T: Serialize>(&self, key: &[u8], value: Option<&T>) -> std::io::Result<u64> {
126-
let serialized = match value {
127-
Some(v) => bincode::serialize(v).unwrap_or_else(|_| OPTION_TOMBSTONE_MARKER.to_vec()),
128-
None => OPTION_TOMBSTONE_MARKER.to_vec(),
129-
};
126+
fn write_option<T: Serialize>(&self, key: &[u8], value: Option<&T>) -> io::Result<u64> {
127+
let key = &prefix_key(OPTION_PREFIX, key);
130128

129+
let serialized = serialize_option(value)?;
131130
self.write(key, &serialized)
132131
}
133132

134133
fn read_option<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, io::Error> {
134+
let key = &prefix_key(OPTION_PREFIX, key);
135+
135136
match self.read(key) {
136-
Some(entry) => {
137-
let data = entry.as_slice();
138-
if data == OPTION_TOMBSTONE_MARKER {
139-
return Ok(None);
140-
}
141-
bincode::deserialize::<T>(data)
142-
.map(Some)
143-
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
144-
}
145-
None => {
146-
return Err(io::Error::new(
147-
ErrorKind::NotFound,
148-
"Key not found in storage",
149-
))
150-
}
137+
Some(entry) => deserialize_option::<T>(entry.as_slice()),
138+
None => Err(io::Error::new(
139+
ErrorKind::NotFound,
140+
"Key not found in storage",
141+
)),
151142
}
152143
}
153144
}

simd-r-drive-extensions/src/utils.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod option_serializer;
2+
pub use option_serializer::{deserialize_option, serialize_option};
3+
4+
pub mod prefix_key;
5+
pub use prefix_key::prefix_key;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::constants::OPTION_TOMBSTONE_MARKER;
2+
use bincode;
3+
use serde::de::DeserializeOwned;
4+
use serde::Serialize;
5+
use std::io::{self, ErrorKind};
6+
7+
/// Serializes an `Option<T>` into a binary representation compatible with `SIMD R Drive`.
8+
///
9+
/// - If `Some(value)`, serializes the value using `bincode`.
10+
/// - If `None`, returns a tombstone marker (`[0xFF, 0xFE]`).
11+
///
12+
/// ## Returns
13+
/// - `Vec<u8>` containing the serialized value or tombstone marker.
14+
/// - `Err(io::Error)`: If serialization fails.
15+
///
16+
/// ## Example
17+
/// ```
18+
/// use simd_r_drive_extensions::utils::option_serializer::serialize_option;
19+
///
20+
/// let some_value = serialize_option(Some(&42)).unwrap();
21+
/// let none_value = serialize_option::<i32>(None).unwrap();
22+
///
23+
/// assert_ne!(some_value, none_value);
24+
/// assert_eq!(none_value, vec![0xFF, 0xFE]); // Tombstone marker for `None`
25+
/// ```
26+
pub fn serialize_option<T: Serialize>(value: Option<&T>) -> io::Result<Vec<u8>> {
27+
match value {
28+
Some(v) => bincode::serialize(v)
29+
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "Failed to serialize Option<T>")),
30+
None => Ok(OPTION_TOMBSTONE_MARKER.to_vec()),
31+
}
32+
}
33+
34+
/// Deserializes an `Option<T>` from binary storage.
35+
///
36+
/// - **⚠️ Non Zero-Copy Warning**: Requires deserialization.
37+
/// - If the data matches the tombstone marker, returns `Ok(None)`.
38+
/// - Otherwise, attempts to deserialize the stored value.
39+
///
40+
/// ## Returns
41+
/// - `Ok(Some(T))` if the data is valid.
42+
/// - `Ok(None)` if the tombstone marker is found.
43+
/// - `Err(io::Error)`: If deserialization fails.
44+
///
45+
/// ## Example
46+
/// ```
47+
/// use simd_r_drive_extensions::utils::option_serializer::{serialize_option, deserialize_option};
48+
///
49+
/// let some_value = serialize_option(Some(&42)).unwrap();
50+
/// let none_value = serialize_option::<i32>(None).unwrap();
51+
///
52+
/// let deserialized_some: Option<i32> = deserialize_option(&some_value).unwrap();
53+
/// let deserialized_none: Option<i32> = deserialize_option(&none_value).unwrap();
54+
///
55+
/// assert_eq!(deserialized_some, Some(42));
56+
/// assert_eq!(deserialized_none, None);
57+
/// ```
58+
pub fn deserialize_option<T: DeserializeOwned>(data: &[u8]) -> Result<Option<T>, io::Error> {
59+
if data == OPTION_TOMBSTONE_MARKER {
60+
return Ok(None);
61+
}
62+
63+
bincode::deserialize::<T>(data)
64+
.map(Some)
65+
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "Failed to deserialize Option<T>"))
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// Utility function to **prefix a binary key** with a given prefix.
2+
///
3+
/// - Ensures the prefixed key remains valid for storage.
4+
/// - Prevents key collisions by ensuring distinct namespaces.
5+
///
6+
/// ## Arguments
7+
/// - `prefix`: The binary prefix to prepend.
8+
/// - `key`: The original binary key.
9+
///
10+
/// ## Returns
11+
/// - A new `Vec<u8>` containing the prefixed key.
12+
///
13+
/// ## Example
14+
/// ```rust
15+
/// use simd_r_drive_extensions::utils::prefix_key;
16+
///
17+
/// let key = b"my_key";
18+
/// let prefixed = prefix_key(b"cache_", key);
19+
///
20+
/// assert_eq!(prefixed, b"cache_my_key".to_vec());
21+
/// ```
22+
pub fn prefix_key(prefix: &[u8], key: &[u8]) -> Vec<u8> {
23+
let mut prefixed_key = Vec::with_capacity(prefix.len() + key.len());
24+
prefixed_key.extend_from_slice(prefix);
25+
prefixed_key.extend_from_slice(key);
26+
prefixed_key
27+
}

0 commit comments

Comments
 (0)