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.

04-expiry/src/main.rs
rust
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.

04-expiry/src/main.rs
rust
"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…