From 320c1d95b35fe8cd59d82923eeb7273efe4e4906 Mon Sep 17 00:00:00 2001 From: May Knott Date: Sun, 24 May 2026 03:06:36 +0330 Subject: [PATCH] fix(cache): refresh response entries on cache hits The response cache is byte-bounded and evicts from an order queue when inserting a new entry would exceed the configured capacity. Before this change, that queue only reflected insertion order: a frequently reused cached response could still be evicted ahead of colder entries if it happened to be inserted earlier. Refresh the cache order on successful, unexpired get calls. The cached bytes are cloned before mutating the order queue, the hit counter behavior is preserved, and expired entries still remove their stored bytes and order entry before recording a miss. Update the eviction regression test so it exercises true least-recently-used behavior: after warming entry a, inserting entry f evicts b rather than the recently read a. Cache size accounting, TTL parsing, cacheability rules, entry-size rejection, and the public ResponseCache API remain unchanged. --- src/cache.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 6a0c6d51..f1b222dc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -53,8 +53,11 @@ impl ResponseCache { let mut inner = self.inner.lock().unwrap(); if let Some(entry) = inner.entries.get(key) { if entry.expires > now { + let bytes = entry.bytes.clone(); + inner.order.retain(|k| k != key); + inner.order.push_back(key.to_string()); self.hits.fetch_add(1, Ordering::Relaxed); - return Some(entry.bytes.clone()); + return Some(bytes); } let size = entry.bytes.len(); inner.entries.remove(key); @@ -215,15 +218,17 @@ mod tests { } #[test] - fn fifo_eviction_when_full() { + fn least_recently_used_entry_is_evicted_when_full() { let c = ResponseCache::new(1000); c.put("a".into(), vec![0u8; 200], Duration::from_secs(60)); c.put("b".into(), vec![0u8; 200], Duration::from_secs(60)); c.put("c".into(), vec![0u8; 200], Duration::from_secs(60)); c.put("d".into(), vec![0u8; 200], Duration::from_secs(60)); c.put("e".into(), vec![0u8; 200], Duration::from_secs(60)); + assert!(c.get("a").is_some()); c.put("f".into(), vec![0u8; 200], Duration::from_secs(60)); - assert!(c.get("a").is_none()); + assert!(c.get("a").is_some()); + assert!(c.get("b").is_none()); assert!(c.get("f").is_some()); }