Instant-based TTLs with lazy delete
Instant is a monotonic clock. Unlike SystemTime, it cannot go backwards from an NTP step. For TTLs we add a Duration to Instant::now() and compare. Sidecar map mirrors what Python and Go do.
struct StoreInner {
store: HashMap<String, String>,
expires: HashMap<String, Instant>,
}
fn check_expired(inner: &mut StoreInner, key: &str) -> bool {
if let Some(ts) = inner.expires.get(key) {
if Instant::now() >= *ts {
inner.store.remove(key);
inner.expires.remove(key);
return true;
}
}
false
}StoreInner has expires: HashMap<String, Instant>. check_expired is called inside every read path.
Because lazy delete is a write. If the key has expired, we mutate both maps to evict it. That makes the read path a mutable operation, just like Go and Python had to do. The compiler enforcing this is the type system catching what Go documents and Python prays for.
"TTL" => {
let mut inner = store.lock().await;
if check_expired(&mut inner, &rest[0]) { return encode_integer(-2); }
match inner.expires.get(&rest[0]) {
Some(ts) => {
let remaining = ts.saturating_duration_since(Instant::now()).as_secs();
encode_integer(remaining as i64)
}
None => encode_integer(if inner.store.contains_key(&rest[0]) { -1 } else { -2 }),
}
}TTL command. saturating_duration_since handles the case where the deadline is already past (would underflow a normal subtraction).
Quiz: Quiz
Loading practice…