Skip to content

Commit be9684e

Browse files
authored
TTL cache extension (#20)
1 parent ece3212 commit be9684e

File tree

7 files changed

+449
-165
lines changed

7 files changed

+449
-165
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ By using SIMD for these performance-critical tasks, `SIMD R Drive` minimizes CPU
220220

221221
## Extensions
222222

223-
[SIMD R Drive Extensions](./simd-r-drive-extensions/) provide additional functionality and introduce extra dependencies.
223+
[SIMD R Drive Extensions](./simd-r-drive-extensions/) provide additional functionality.
224224

225225
## License
226226

simd-r-drive-extensions/README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Work in progress.**
44

5-
`simd-r-drive-extensions` provides optional utilities for working with `Option<T>` in [SIMD R Drive](https://crates.io/crates/simd-r-drive).
5+
`simd-r-drive-extensions` provides optional utilities for working with `Option<T>` and TTL-based caching in [SIMD R Drive](https://crates.io/crates/simd-r-drive).
66

77
[Documentation](https://docs.rs/simd-r-drive-extensions/latest/simd_r_drive_extensions/)
88

@@ -14,6 +14,7 @@ cargo add simd-r-drive-extensions
1414

1515
## Usage
1616

17+
### Working with `Option<T>`
1718
```rust
1819
use simd_r_drive::DataStore;
1920
use simd_r_drive_extensions::StorageOptionExt;
@@ -35,28 +36,42 @@ assert_eq!(
3536
None
3637
);
3738

38-
// Check if the key exists in storage, regardless of whether it's `Some` or `None`
39-
if let Ok(none_option) = storage.read_option::<i32>(b"key_with_none_value") {
40-
assert!(none_option.is_none());
41-
} else {
42-
// Just to check the example
43-
panic!("Failed to read key: `key_with_none_value` does not exist or read error occurred.");
44-
}
45-
46-
// Alternative, concise check
47-
let none_option = storage.read_option::<i32>(b"key_with_none_value").unwrap();
48-
assert!(none_option.is_none()); // Ensures `Option<T>` exists
49-
5039
// Errors on non-existent keys
5140
assert!(storage.read_option::<i32>(b"non_existent_key").is_err());
41+
```
42+
43+
### Working with TTL-based Caching
44+
```rust
45+
use simd_r_drive::DataStore;
46+
use simd_r_drive_extensions::StorageCacheExt;
47+
use std::path::PathBuf;
48+
use std::thread::sleep;
49+
use std::time::Duration;
5250

51+
let storage = DataStore::open(&PathBuf::from("test_store.bin")).unwrap();
52+
53+
// Write value with a TTL of 5 seconds
54+
storage.write_with_ttl(b"key_with_ttl", &42, 5).unwrap();
55+
assert_eq!(
56+
storage.read_with_ttl::<i32>(b"key_with_ttl").expect("Failed to read key"),
57+
Some(42)
58+
);
59+
60+
// Wait for TTL to expire
61+
sleep(Duration::from_secs(6));
62+
assert_eq!(
63+
storage.read_with_ttl::<i32>(b"key_with_ttl").expect("Failed to read key"),
64+
None // Key should be expired and removed
65+
);
5366
```
5467

5568
## Implementation Details
5669

5770
- Uses a predefined tombstone marker (`[0xFF, 0xFE]`) to represent `None`.
71+
- TTL values are stored as a **binary prefix** before the actual value.
5872
- Values are serialized using [bincode](https://crates.io/crates/bincode).
5973
- ⚠️ Unlike [SIMD R Drive](https://crates.io/crates/simd-r-drive), values are non-zero-copy, as they require deserialization.
74+
- TTL-based storage will **automatically evict expired values upon read** to prevent stale data.
6075

6176
## License
6277

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

Lines changed: 4 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,8 @@
11
#[cfg(doctest)]
22
doc_comment::doctest!("../README.md");
33

4-
use bincode;
5-
use serde::de::DeserializeOwned;
6-
use serde::Serialize;
7-
use simd_r_drive::DataStore;
8-
use std::io::{self, ErrorKind};
4+
mod storage_option_ext;
5+
pub use storage_option_ext::*;
96

10-
/// Special marker for explicitly storing `None` values in binary storage.
11-
/// This ensures that `None` is distinguishable from an empty or default value.
12-
const OPTION_TOMBSTONE_MARKER: [u8; 2] = [0xFF, 0xFE];
13-
14-
#[cfg(any(test, debug_assertions))]
15-
pub const TEST_OPTION_TOMBSTONE_MARKER: [u8; 2] = OPTION_TOMBSTONE_MARKER;
16-
17-
/// # Storage Utilities for Handling `Option<T>`
18-
///
19-
/// This trait provides methods to store and retrieve `Option<T>` values
20-
/// in a `DataStore`, ensuring that `None` values are explicitly handled.
21-
///
22-
/// ## Purpose
23-
/// - **Prevents ambiguity**: Ensures `None` is stored and retrieved correctly.
24-
/// - **Efficient storage**: Uses a compact representation.
25-
/// - **Binary-safe**: Avoids unintended interpretation of missing values.
26-
///
27-
/// ## Implementation Details
28-
/// - **`Some(value)`**: Serialized using `bincode`.
29-
/// - **`None`**: Explicitly stored using a dedicated tombstone marker (`[0xFF, 0xFE]`).
30-
///
31-
/// ## Example Usage
32-
///
33-
/// ```rust
34-
/// use simd_r_drive::DataStore;
35-
/// use simd_r_drive_extensions::StorageOptionExt;
36-
/// use std::path::PathBuf;
37-
///
38-
/// let storage = DataStore::open(&PathBuf::from("test_store.bin")).unwrap();
39-
///
40-
/// // Store `Some(value)`
41-
/// storage.write_option(b"key1", Some(&42)).unwrap();
42-
///
43-
/// // Store `None` (tombstone)
44-
/// storage.write_option::<i32>(b"key2", None).unwrap();
45-
///
46-
/// // Read values
47-
/// assert_eq!(storage.read_option::<i32>(b"key1").unwrap(), Some(42));
48-
/// assert_eq!(storage.read_option::<i32>(b"key2").unwrap(), None);
49-
/// ```
50-
pub trait StorageOptionExt {
51-
fn write_option<T: Serialize>(&self, key: &[u8], value: Option<&T>) -> std::io::Result<u64>;
52-
fn read_option<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, std::io::Error>;
53-
}
54-
55-
/// Implements `StorageOptionExt` for `DataStore`
56-
impl StorageOptionExt for DataStore {
57-
/// Writes an `Option<T>` into the `DataStore`, ensuring `None` values are preserved.
58-
///
59-
/// - `Some(value)`: Serialized using `bincode`.
60-
/// - `None`: Stored in a way that allows correct retrieval.
61-
///
62-
/// ## Arguments
63-
/// - `key`: The binary key under which the value is stored.
64-
/// - `value`: An optional reference to `T`, where `None` is handled appropriately.
65-
///
66-
/// ## Returns
67-
/// - `Ok(offset)`: The **file offset** where the data was written.
68-
/// - `Err(std::io::Error)`: If the write operation fails.
69-
///
70-
/// ## Example
71-
/// ```rust
72-
/// use simd_r_drive::DataStore;
73-
/// use simd_r_drive_extensions::StorageOptionExt;
74-
/// use std::path::PathBuf;
75-
///
76-
/// let storage = DataStore::open(&PathBuf::from("store.bin")).unwrap();
77-
///
78-
/// // Write `Some(value)`
79-
/// storage.write_option(b"key_with_some_value", Some(&123)).unwrap();
80-
///
81-
/// // Write `None` (tombstone)
82-
/// storage.write_option::<i32>(b"key_with_none_value", None).unwrap();
83-
/// ```
84-
fn write_option<T: Serialize>(&self, key: &[u8], value: Option<&T>) -> std::io::Result<u64> {
85-
let serialized = match value {
86-
Some(v) => bincode::serialize(v).unwrap_or_else(|_| OPTION_TOMBSTONE_MARKER.to_vec()),
87-
None => OPTION_TOMBSTONE_MARKER.to_vec(),
88-
};
89-
90-
self.write(key, &serialized)
91-
}
92-
93-
/// Reads an `Option<T>` from storage.
94-
///
95-
/// - **⚠️ Non Zero-Copy Warning**: Requires deserialization.
96-
/// - **Returns `Ok(None)`** if the key exists and explicitly stores the tombstone marker (`[0xFF, 0xFE]`).
97-
/// - **Returns `Err(ErrorKind::NotFound)`** if the key does not exist.
98-
/// - **Returns `Err(ErrorKind::InvalidData)`** if deserialization fails.
99-
///
100-
/// ## Arguments
101-
/// - `key`: The binary key to retrieve.
102-
///
103-
/// ## Returns
104-
/// - `Ok(Some(T))`: If deserialization succeeds and is `Some`.
105-
/// - `Ok(None)`: If the key represents `None`.
106-
/// - `Err(std::io::Error)`: If the key does not exist or if deserialization fails.
107-
///
108-
/// ## Example
109-
/// ```rust
110-
/// use simd_r_drive::DataStore;
111-
/// use simd_r_drive_extensions::StorageOptionExt;
112-
/// use std::path::PathBuf;
113-
///
114-
/// let storage = DataStore::open(&PathBuf::from("store.bin")).unwrap();
115-
///
116-
/// storage.write_option(b"key_with_some_value", Some(&789)).unwrap();
117-
/// storage.write_option::<i32>(b"key_with_none_value", None).unwrap();
118-
///
119-
/// assert_eq!(storage.read_option::<i32>(b"key_with_some_value").unwrap(), Some(789));
120-
/// assert_eq!(storage.read_option::<i32>(b"key_with_none_value").unwrap(), None);
121-
///
122-
/// if let Ok(none_option) = storage.read_option::<i32>(b"key_with_none_value") {
123-
/// assert!(none_option.is_some() || none_option.is_none()); // Explicitly checking Option type
124-
/// }
125-
///
126-
/// // Alternative, concise check
127-
/// let none_option = storage.read_option::<i32>(b"key_with_none_value").unwrap();
128-
/// assert!(none_option.is_none() || none_option.is_some()); // Ensures `Option<T>` exists
129-
///
130-
/// // Errors on non-existent keys
131-
/// assert!(storage.read_option::<i32>(b"non_existent_key").is_err());
132-
/// ```
133-
///
134-
/// # Safety
135-
/// - This function **allocates memory** for deserialization.
136-
fn read_option<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, io::Error> {
137-
match self.read(key) {
138-
Some(entry) => {
139-
let data = entry.as_slice();
140-
if data == OPTION_TOMBSTONE_MARKER {
141-
return Ok(None);
142-
}
143-
bincode::deserialize::<T>(data)
144-
.map(Some)
145-
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
146-
}
147-
None => {
148-
return Err(io::Error::new(
149-
ErrorKind::NotFound,
150-
"Key not found in storage",
151-
))
152-
}
153-
}
154-
}
155-
}
7+
mod storage_cache_ext;
8+
pub use storage_cache_ext::*;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use serde::de::DeserializeOwned;
2+
use serde::Serialize;
3+
use simd_r_drive::DataStore;
4+
use std::io::{self, ErrorKind};
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
/// **Prefix-based TTL storage for cache expiration**
8+
/// Stores a timestamp (in seconds) before the actual value.
9+
pub trait StorageCacheExt {
10+
/// Writes a value with a TTL (Time-To-Live).
11+
///
12+
/// - Stores the expiration timestamp as a **binary prefix** before the actual data.
13+
/// - If the key exists, it will be **overwritten**.
14+
///
15+
/// ## Arguments
16+
/// - `key`: The binary key to store.
17+
/// - `value`: The value to be stored.
18+
/// - `ttl_secs`: The TTL in **seconds** (relative to current time).
19+
///
20+
/// ## Returns
21+
/// - `Ok(offset)`: The **file offset** where the data was written.
22+
/// - `Err(std::io::Error)`: If the write operation fails.
23+
fn write_with_ttl<T: Serialize>(&self, key: &[u8], value: &T, ttl_secs: u64)
24+
-> io::Result<u64>;
25+
26+
/// Reads a value, checking TTL expiration.
27+
///
28+
/// - If the TTL has expired, the key is **automatically evicted**, and `None` is returned.
29+
/// - If the key does not exist, returns `Err(ErrorKind::NotFound)`.
30+
/// - If deserialization fails, returns `Err(ErrorKind::InvalidData)`.
31+
///
32+
/// ## Returns
33+
/// - `Ok(Some(T))`: If the TTL is still valid and the value is readable.
34+
/// - `Ok(None)`: If the TTL has expired and the entry has been evicted.
35+
/// - `Err(std::io::Error)`: If the key is missing or deserialization fails.
36+
fn read_with_ttl<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, io::Error>;
37+
}
38+
39+
/// Implements TTL-based caching for `DataStore`
40+
impl StorageCacheExt for DataStore {
41+
fn write_with_ttl<T: Serialize>(
42+
&self,
43+
key: &[u8],
44+
value: &T,
45+
ttl_secs: u64,
46+
) -> io::Result<u64> {
47+
let expiration_timestamp = SystemTime::now()
48+
.duration_since(UNIX_EPOCH)
49+
.expect("Time went backwards")
50+
.as_secs()
51+
.saturating_add(ttl_secs); // Avoid overflow
52+
53+
let mut data = expiration_timestamp.to_le_bytes().to_vec();
54+
let serialized_value = bincode::serialize(value)
55+
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "Serialization failed"))?;
56+
data.extend_from_slice(&serialized_value);
57+
58+
self.write(key, &data)
59+
}
60+
61+
fn read_with_ttl<T: DeserializeOwned>(&self, key: &[u8]) -> Result<Option<T>, io::Error> {
62+
match self.read(key) {
63+
Some(entry) => {
64+
let data = entry.as_slice();
65+
66+
if data.len() < 8 {
67+
return Err(io::Error::new(
68+
ErrorKind::InvalidData,
69+
"Data too short to contain TTL",
70+
));
71+
}
72+
73+
let expiration_timestamp = u64::from_le_bytes(data[..8].try_into().unwrap());
74+
let now = SystemTime::now()
75+
.duration_since(UNIX_EPOCH)
76+
.expect("Time went backwards")
77+
.as_secs();
78+
79+
if now >= expiration_timestamp {
80+
self.delete_entry(key).ok(); // Remove expired entry
81+
return Ok(None);
82+
}
83+
84+
bincode::deserialize::<T>(&data[8..])
85+
.map(Some)
86+
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "Deserialization failed"))
87+
}
88+
None => Err(io::Error::new(ErrorKind::NotFound, "Key not found")),
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)