From 8cfc24836fe46bf1a179088145fc2e3389472ee0 Mon Sep 17 00:00:00 2001 From: Dmitry Agafonov <42949186+Malkiz223@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:48:21 +0300 Subject: [PATCH] Bump the generation when a read promotes an LRUCache entry --- src/internal/linked_list.rs | 11 +++++++++++ src/policies/fifopolicy.rs | 1 + src/policies/lfupolicy.rs | 1 + src/policies/lrupolicy.rs | 9 ++++++++- src/policies/nopolicy.rs | 1 + src/policies/rrpolicy.rs | 1 + src/policies/traits.rs | 1 + src/policies/ttlpolicy.rs | 1 + src/policies/vttlpolicy.rs | 1 + src/policies/wrapped.rs | 2 +- src/pyclasses/cache.rs | 10 +++++----- src/pyclasses/fifocache.rs | 10 +++++----- src/pyclasses/lfucache.rs | 10 +++++----- src/pyclasses/lrucache.rs | 10 +++++----- src/pyclasses/rrcache.rs | 10 +++++----- src/pyclasses/ttlcache.rs | 12 ++++++------ src/pyclasses/vttlcache.rs | 12 ++++++------ tests/test_impls.py | 32 ++++++++++++++++++++++++++++++++ 18 files changed, 96 insertions(+), 39 deletions(-) diff --git a/src/internal/linked_list.rs b/src/internal/linked_list.rs index 4572e07..91f21ec 100644 --- a/src/internal/linked_list.rs +++ b/src/internal/linked_list.rs @@ -571,6 +571,17 @@ impl Cursor { unsafe { list.push_front_node(links) }; } + /// Returns `true` if the node this cursor points to is the last node of `list`. + /// + /// # Safety + /// + /// Same contract as [`move_to_front`](Self::move_to_front). + #[inline] + pub unsafe fn is_back(&self, list: &LinkedList) -> bool { + // SAFETY: the sentinel is always valid; caller guarantees the node is linked. + unsafe { (*list.sentinel.as_ptr()).prev == self.links().as_ptr() } + } + /// Moves the node this cursor points to the back of `list`. /// /// # Safety diff --git a/src/policies/fifopolicy.rs b/src/policies/fifopolicy.rs index e071bc8..4afc7fc 100644 --- a/src/policies/fifopolicy.rs +++ b/src/policies/fifopolicy.rs @@ -255,6 +255,7 @@ impl PolicyExt for FIFOPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let eq = |index: &usize| get_handle!(&self, *index).key().py_eq(py, key); match self.table.get(key.hash(), eq)? { diff --git a/src/policies/lfupolicy.rs b/src/policies/lfupolicy.rs index f22c30b..d5ea80a 100644 --- a/src/policies/lfupolicy.rs +++ b/src/policies/lfupolicy.rs @@ -320,6 +320,7 @@ impl PolicyExt for LFUPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let cursor = self .table diff --git a/src/policies/lrupolicy.rs b/src/policies/lrupolicy.rs index 9142a5f..73f248b 100644 --- a/src/policies/lrupolicy.rs +++ b/src/policies/lrupolicy.rs @@ -169,6 +169,7 @@ impl PolicyExt for LRUPolicy { &mut self, py: pyo3::Python, key: &::Key, + shared: &Self::Shared, ) -> pyo3::PyResult> { unsafe { let bucket = self @@ -177,7 +178,13 @@ impl PolicyExt for LRUPolicy { match bucket { Some(cursor) => { - cursor.move_to_back(&mut self.list); + // moving the entry is a mutation, so it bumps the generation like + // any other; a hit on the most recent entry moves nothing + if !cursor.is_back(&self.list) { + shared.generation_version().increment(); + cursor.move_to_back(&mut self.list); + } + Ok(Some(cursor.element())) } None => Ok(None), diff --git a/src/policies/nopolicy.rs b/src/policies/nopolicy.rs index 6511dd9..b45f05f 100644 --- a/src/policies/nopolicy.rs +++ b/src/policies/nopolicy.rs @@ -134,6 +134,7 @@ impl PolicyExt for NoPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let bucket = self.table.find(key.hash(), |x| key.py_eq(py, x.key()))?; Ok(bucket.map(|x| unsafe { x.as_ref() })) diff --git a/src/policies/rrpolicy.rs b/src/policies/rrpolicy.rs index 7db9ba5..fd60c6e 100644 --- a/src/policies/rrpolicy.rs +++ b/src/policies/rrpolicy.rs @@ -136,6 +136,7 @@ impl PolicyExt for RRPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let bucket = self.table.find(key.hash(), |x| key.py_eq(py, x.key()))?; Ok(bucket.map(|x| unsafe { x.as_ref() })) diff --git a/src/policies/traits.rs b/src/policies/traits.rs index 7509949..9a17dfa 100644 --- a/src/policies/traits.rs +++ b/src/policies/traits.rs @@ -105,6 +105,7 @@ pub trait PolicyExt: Sized { &mut self, py: pyo3::Python, key: &::Key, + shared: &Self::Shared, ) -> pyo3::PyResult>; /// Returns a [`PolicyEntry`] for the slot at `hash` / `eq`. diff --git a/src/policies/ttlpolicy.rs b/src/policies/ttlpolicy.rs index e53a358..a4159b5 100644 --- a/src/policies/ttlpolicy.rs +++ b/src/policies/ttlpolicy.rs @@ -369,6 +369,7 @@ impl PolicyExt for TTLPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let eq = |index: &usize| get_handle!(&self, *index).key().py_eq(py, key); diff --git a/src/policies/vttlpolicy.rs b/src/policies/vttlpolicy.rs index ee67599..54a596c 100644 --- a/src/policies/vttlpolicy.rs +++ b/src/policies/vttlpolicy.rs @@ -311,6 +311,7 @@ impl PolicyExt for VTTLPolicy { &mut self, py: pyo3::Python, key: &::Key, + _shared: &Self::Shared, ) -> pyo3::PyResult> { let cursor = self .table diff --git a/src/policies/wrapped.rs b/src/policies/wrapped.rs index 0de1b2b..c4a5b32 100644 --- a/src/policies/wrapped.rs +++ b/src/policies/wrapped.rs @@ -112,7 +112,7 @@ impl Wrapped

{ ) -> pyo3::PyResult { let mut lock = self.inner.lock(); - let handle = lock.get(py, key)?; + let handle = lock.get(py, key, &self.shared)?; Ok(handle.is_some()) } diff --git a/src/pyclasses/cache.rs b/src/pyclasses/cache.rs index 13fe4d4..42d8f3c 100644 --- a/src/pyclasses/cache.rs +++ b/src/pyclasses/cache.rs @@ -279,7 +279,7 @@ impl PyCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -302,7 +302,7 @@ impl PyCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -334,7 +334,7 @@ impl PyCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -383,7 +383,7 @@ impl PyCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -393,7 +393,7 @@ impl PyCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } diff --git a/src/pyclasses/fifocache.rs b/src/pyclasses/fifocache.rs index 3ee15b1..f657d0e 100644 --- a/src/pyclasses/fifocache.rs +++ b/src/pyclasses/fifocache.rs @@ -285,7 +285,7 @@ impl PyFIFOCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -308,7 +308,7 @@ impl PyFIFOCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -340,7 +340,7 @@ impl PyFIFOCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -389,7 +389,7 @@ impl PyFIFOCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -399,7 +399,7 @@ impl PyFIFOCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } diff --git a/src/pyclasses/lfucache.rs b/src/pyclasses/lfucache.rs index 0d4a494..5632223 100644 --- a/src/pyclasses/lfucache.rs +++ b/src/pyclasses/lfucache.rs @@ -303,7 +303,7 @@ impl PyLFUCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -326,7 +326,7 @@ impl PyLFUCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -358,7 +358,7 @@ impl PyLFUCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -408,7 +408,7 @@ impl PyLFUCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -418,7 +418,7 @@ impl PyLFUCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } diff --git a/src/pyclasses/lrucache.rs b/src/pyclasses/lrucache.rs index cb7c02f..8804198 100644 --- a/src/pyclasses/lrucache.rs +++ b/src/pyclasses/lrucache.rs @@ -312,7 +312,7 @@ impl PyLRUCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -335,7 +335,7 @@ impl PyLRUCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -367,7 +367,7 @@ impl PyLRUCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -416,7 +416,7 @@ impl PyLRUCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -426,7 +426,7 @@ impl PyLRUCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } diff --git a/src/pyclasses/rrcache.rs b/src/pyclasses/rrcache.rs index 63a6575..084e85b 100644 --- a/src/pyclasses/rrcache.rs +++ b/src/pyclasses/rrcache.rs @@ -283,7 +283,7 @@ impl PyRRCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -306,7 +306,7 @@ impl PyRRCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -338,7 +338,7 @@ impl PyRRCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -387,7 +387,7 @@ impl PyRRCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -397,7 +397,7 @@ impl PyRRCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } diff --git a/src/pyclasses/ttlcache.rs b/src/pyclasses/ttlcache.rs index 992d0f0..9037bad 100644 --- a/src/pyclasses/ttlcache.rs +++ b/src/pyclasses/ttlcache.rs @@ -288,7 +288,7 @@ impl PyTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -311,7 +311,7 @@ impl PyTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -343,7 +343,7 @@ impl PyTTLCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -393,7 +393,7 @@ impl PyTTLCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -403,7 +403,7 @@ impl PyTTLCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -724,7 +724,7 @@ impl PyTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { let dur = x .expires_at() .duration_since(std::time::SystemTime::now()) diff --git a/src/pyclasses/vttlcache.rs b/src/pyclasses/vttlcache.rs index 0644453..bd30a03 100644 --- a/src/pyclasses/vttlcache.rs +++ b/src/pyclasses/vttlcache.rs @@ -271,7 +271,7 @@ impl PyVTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -294,7 +294,7 @@ impl PyVTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - match policy.get(py, &key)? { + match policy.get(py, &key, inner.shared())? { Some(x) => Ok(x.value().clone_ref(py)), None => Err(new_py_error!( PyKeyError, @@ -324,7 +324,7 @@ impl PyVTTLCache { let shared = inner.shared(); let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -370,7 +370,7 @@ impl PyVTTLCache { { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } } @@ -380,7 +380,7 @@ impl PyVTTLCache { let mut policy = inner.policy(); - if let Some(x) = policy.get(py, &key)? { + if let Some(x) = policy.get(py, &key, inner.shared())? { return Ok(x.value().clone_ref(py)); } @@ -662,7 +662,7 @@ impl PyVTTLCache { let inner = self.0.get(); let mut policy = inner.policy(); - if let Some(handle) = policy.get(py, &key)? { + if let Some(handle) = policy.get(py, &key, inner.shared())? { let dur = match handle.expires_at() { Some(x) => { let secs = x diff --git a/tests/test_impls.py b/tests/test_impls.py index 51725a0..72c03a1 100644 --- a/tests/test_impls.py +++ b/tests/test_impls.py @@ -1,3 +1,5 @@ +import subprocess +import sys import time import typing from datetime import datetime, timedelta @@ -504,6 +506,23 @@ def create_cache( ) +READ_UNDER_LIVE_ITERATOR = """ +import cachebox + +cache = cachebox.LRUCache(10) +for key in ("a", "b", "c"): + cache[key] = 1 + +it = cache.keys() +assert "a" in cache + +try: + list(it) +except RuntimeError: + print("ok") +""" + + class TestLRUCachePolicy(mixins.BaseMixin): def create_cache( self, @@ -533,6 +552,19 @@ def test_evicts_lru_when_full(self): assert "a" not in c assert "d" in c + def test_read_under_live_iterator_invalidates_it(self): + # a read promotes the key and relinks the list; without the generation + # bump the iterator walks rewired memory and the process dies, so the + # check runs in a child process + done = subprocess.run( + [sys.executable, "-c", READ_UNDER_LIVE_ITERATOR], + capture_output=True, + text=True, + timeout=60, + ) + + assert done.stdout.strip() == "ok", done.stderr or f"exit code {done.returncode}" + def test_does_not_evict_recently_read_key(self): c = self.create_cache(3) c.insert("a", 1)