Intermediate Projects
In-Memory Key-Value Store
An in-memory key-value store (modeled after Redis and Memcached) stores key-value records with optional Time-To-Live (TTL) expiration, atomic integer increments (INCRBY), prefix scans, and a Write-Ahead Log (WAL) / command snapshot format.
In-Memory KV Store with Lazy + Active TTL Expiration:
`Command::Set { key, val, ttl }`
|
v
`HashMap<String, Record { value: Vec<u8>, expires_at: Option<u64> }>`
|
+-- `get(key, now_ms)`:
| If `expires_at <= Some(now_ms)` ---> Expired! Return `None` (Lazy Eviction)
|
+-- `purge_expired(now_ms)`:
`map.retain(|_, rec| !rec.is_expired(now_ms))` (Active Compaction)
Core Commands and TTL Architecture
Passing a monotonic or epoch millisecond timestamp now_ms: u64 (or Clock trait) into operations keeps TTL expiration 100% deterministic in tests and production replay logs:
| Operation | Signature | Behavior |
|---|---|---|
set |
(&mut self, key, val, ttl_ms: Option<u64>, now_ms: u64) |
Inserts or overwrites key, computing expires_at = ttl_ms.map(|ttl| now_ms + ttl). |
get |
(&mut self, key: &str, now_ms: u64) -> Option<&str> |
Checks if the record has expired at now_ms; if expired, removes it lazily and returns None! |
incr_by |
(&mut self, key: &str, delta: i64, now_ms: u64) -> Result<i64, KvError> |
Parses existing value as i64 (defaulting to 0 if missing or expired), adds delta, and stores the updated string. |
purge_expired |
(&mut self, now_ms: u64) -> usize |
Sweeps and removes all expired keys in $O(n)$ using HashMap::retain. |
Complete Runnable Implementation
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Record {
value: String,
expires_at_ms: Option<u64>,
}
impl Record {
#[inline]
fn is_expired(&self, now_ms: u64) -> bool {
self.expires_at_ms.is_some_and(|exp| now_ms >= exp)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum KvError {
NotAnInteger(String),
Overflow,
}
#[derive(Default)]
pub struct KvStore {
entries: HashMap<String, Record>,
}
impl KvStore {
pub fn new() -> Self {
Self::default()
}
pub fn set(
&mut self,
key: impl Into<String>,
value: impl Into<String>,
ttl_ms: Option<u64>,
now_ms: u64,
) {
let expires_at_ms = ttl_ms.map(|ttl| now_ms.saturating_add(ttl));
self.entries.insert(
key.into(),
Record {
value: value.into(),
expires_at_ms,
},
);
}
/// Retrieves `key` if present and not expired; lazily evicts `key` if expired!
pub fn get(&mut self, key: &str, now_ms: u64) -> Option<&str> {
if self.entries.get(key).is_some_and(|r| r.is_expired(now_ms)) {
self.entries.remove(key);
return None;
}
self.entries.get(key).map(|r| r.value.as_str())
}
pub fn delete(&mut self, key: &str) -> bool {
self.entries.remove(key).is_some()
}
/// Atomically increments an integer value stored at `key` (initializing to 0 if absent/expired).
pub fn incr_by(&mut self, key: &str, delta: i64, now_ms: u64) -> Result<i64, KvError> {
let current_rec = self.entries.get(key).filter(|r| !r.is_expired(now_ms));
let (current_val, preserved_expiry) = match current_rec {
Some(rec) => {
let parsed = rec
.value
.parse::<i64>()
.map_err(|_| KvError::NotAnInteger(rec.value.clone()))?;
(parsed, rec.expires_at_ms)
}
None => (0i64, None),
};
let next_val = current_val.checked_add(delta).ok_or(KvError::Overflow)?;
self.entries.insert(
key.to_string(),
Record {
value: next_val.to_string(),
expires_at_ms: preserved_expiry,
},
);
Ok(next_val)
}
/// Actively sweeps all expired keys, returning the number of keys evicted.
pub fn purge_expired(&mut self, now_ms: u64) -> usize {
let before = self.entries.len();
self.entries.retain(|_, rec| !rec.is_expired(now_ms));
before - self.entries.len()
}
}
fn main() {
let mut store = KvStore::new();
let t0 = 1_000u64;
// 1. Set a session token with a 500ms TTL:
store.set("session:alice", "tok_xyz", Some(500), t0);
assert_eq!(store.get("session:alice", t0 + 499), Some("tok_xyz"));
// At t0 + 500ms, the key has expired and is lazily evicted:
assert_eq!(store.get("session:alice", t0 + 500), None);
// 2. Test atomic `incr_by` counter:
assert_eq!(store.incr_by("views:home", 1, t0), Ok(1));
assert_eq!(store.incr_by("views:home", 10, t0), Ok(11));
assert_eq!(store.get("views:home", t0), Some("11"));
println!("Counter views:home = {:?}", store.get("views:home", t0));
}
Counter views:home = Some("11")