From d071239134bf7f2af6590bb902a0335da570bac0 Mon Sep 17 00:00:00 2001 From: Roman Varnava Date: Wed, 28 Oct 2026 19:09:17 +0300 Subject: [PATCH 1/2] test: iterate while other threads mutate the cache Eight threads mix get/set/pop with walking keys()/values()/items() and calling len()/bool() on the views, in a child process so a fault fails the test instead of killing pytest. items() must yield key -> key pairs without duplicates in one pass, which also catches stale reads that don't fault. On free-threaded builds this crashes with SIGSEGV on main (FIFO, LFU, LRU and TTL in every local run, VTTL in some). faulthandler dumps all threads if the child hangs. --- tests/mixins.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/mixins.py b/tests/mixins.py index f76e867..811d918 100644 --- a/tests/mixins.py +++ b/tests/mixins.py @@ -535,6 +535,83 @@ def make(): print("ok") """ +ITERATING_WHILE_OTHER_THREADS_MUTATE = """ +import faulthandler +import random +import sys +import threading +import time + +import cachebox + +name = sys.argv[1] +cls = getattr(cachebox, name) +if name == "TTLCache": + cache = cls(500, global_ttl=60) +elif name == "Cache": + cache = cls(0) +else: + cache = cls(500) + +if name == "VTTLCache": + def put(key): + cache.insert(key, key, 60) +else: + def put(key): + cache[key] = key + +stop = threading.Event() +errors = [] +# a hang prints every thread's stack before the parent's timeout +faulthandler.dump_traceback_later(50, exit=True) + + +def worker(seed): + rng = random.Random(seed) + while not stop.is_set(): + key = rng.randrange(2000) + op = rng.random() + try: + if op < 0.25: + cache.get(key) + elif op < 0.55: + put(key) + elif op < 0.7: + cache.pop(key, None) + else: + if rng.random() < 0.5: + view = rng.choice((cache.keys, cache.values))() + bool(view), len(view) + for _ in view: + pass + else: + # every entry is key -> key: a torn or reused-node read shows up here + view = cache.items() + bool(view), len(view) + seen = set() + for k, v in view: + if k != v or k in seen: + raise AssertionError(f"items() yielded {k!r} -> {v!r}") + seen.add(k) + except RuntimeError: + pass # the cache changed during iteration: expected + except Exception as e: + errors.append(repr(e)) + return + + +threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)] +for t in threads: + t.start() +time.sleep(2) +stop.set() +for t in threads: + t.join() + +assert not errors, errors[:3] +print("ok") +""" + class IterationMixin(BaseMixin): def test_keys_returns_all_keys(self): @@ -577,6 +654,33 @@ def test_walking_a_cache_nothing_else_holds(self): done.stderr or f"exit code {done.returncode}" ) + def test_iterating_while_other_threads_mutate(self): + # Free-threaded builds: an iterator must not read entries another thread is + # changing. Reading freed memory faults instead of failing, hence a child process. + name = type(self.create_cache()).__name__ + + try: + done = subprocess.run( + [ + sys.executable, + "-c", + ITERATING_WHILE_OTHER_THREADS_MUTATE, + name, + ], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + except subprocess.TimeoutExpired: + pytest.fail( + f"{name}: iterating while other threads mutate never finished" + ) + + assert done.stdout.strip() == "ok", ( + done.stderr or f"exit code {done.returncode}" + ) + @pytest.mark.skipif( platform.python_implementation() == "PyPy", reason="PyPy's GC does not collect cycles through the cache objects: " From f2cedee6aa37c4d853a59df1f9bf55524b663d7d Mon Sep 17 00:00:00 2001 From: Roman Varnava Date: Wed, 28 Oct 2026 19:09:17 +0300 Subject: [PATCH 2/2] fix: views read through their position only under the cache lock Views (keys/values/items, LFU items_with_frequency, TTL items_with_expire) hold raw positions into the policy storage and rely on the generation version to notice changes. They compared the generation without holding the cache lock and then read, so on free-threaded builds another thread could free or move entries in between (#94 reported this crash; #98 made replace() bump the generation, but the check-then-read race remained). - implement_view_guard! now generates lock_and_check(): take the cache lock, then compare the generation, and return the guard so it is held while reading. The unlocked check_generation() is removed, so every view method goes through the lock (__next__, __len__, and TTL/VTTL __bool__). - View fields hold the typed cache (Py) to reach its lock. - TTL view constructors created the iterator and read the generation under two separate locks; a mutation in between made stale positions look current. Both now happen under one lock, as VTTL already did. - docs: iterator methods take the cache lock; calling them from code that runs inside an operation on the same cache (a key's __eq__, a value's __del__) blocks, like cache[key] there. Refs #94 --- docs/docs/tips.md | 7 ++++++- src/macro_rules.rs | 27 ++++++++++++++++++------ src/pyclasses/cache.rs | 14 ++++++------- src/pyclasses/fifocache.rs | 14 ++++++------- src/pyclasses/lfucache.rs | 16 +++++++-------- src/pyclasses/lrucache.rs | 14 ++++++------- src/pyclasses/rrcache.rs | 14 ++++++------- src/pyclasses/ttlcache.rs | 42 ++++++++++++++++++++++++++------------ src/pyclasses/vttlcache.rs | 18 ++++++++-------- 9 files changed, 101 insertions(+), 65 deletions(-) diff --git a/docs/docs/tips.md b/docs/docs/tips.md index f357558..994e2df 100644 --- a/docs/docs/tips.md +++ b/docs/docs/tips.md @@ -370,4 +370,9 @@ for k in it: ``` Do not modify the cache while an iterator is alive — every method on that iterator raises -`RuntimeError` if the cache changed. \ No newline at end of file +`RuntimeError` if the cache changed. + +Iterator methods take the cache's lock, so another thread can iterate safely while the cache +is being modified (it gets the `RuntimeError`). Don't call them from code that runs *inside* a +cache operation on the same cache, such as a key's `__eq__` or a value's `__del__`: that waits +for the lock it is already holding and never returns, the same as calling `cache[key]` there. diff --git a/src/macro_rules.rs b/src/macro_rules.rs index 8640cec..c50e337 100644 --- a/src/macro_rules.rs +++ b/src/macro_rules.rs @@ -24,22 +24,37 @@ macro_rules! implement_pyclass { }; } -/// Implements the generation-version guard shared by every cache view type. +/// Implements the guard shared by every cache view type. +/// +/// Views hold raw positions into the policy storage. A position is valid only while +/// the cache is unchanged, and every mutation bumps the generation version while +/// holding the cache lock. So a view must take the cache lock **first**, then compare +/// the generation, and keep the lock while it reads through its position. Checking +/// without the lock races with other threads on free-threaded builds (#94). +/// +/// Lock order: cache lock, then the view's own iterator mutex. /// /// # Example /// /// ```ignore -/// implement_view_guard!(PyCacheKeys); +/// implement_view_guard!(PyCacheKeys, nopolicy::NoPolicy); /// ``` #[macro_export] macro_rules! implement_view_guard { - ($name:ident) => { + ($name:ident, $policy:ty) => { impl $name { - /// Fails if the cache changed after this view was created. + /// Locks the cache and fails if it changed after this view was created. + /// + /// Hold the returned guard for as long as entries are read. #[inline] - fn check_generation(&self) -> pyo3::PyResult<()> { + fn lock_and_check<'a>( + &'a self, + py: pyo3::Python<'_>, + ) -> pyo3::PyResult> { + let policy = self.cache.get().0.get()?.policy_py_attached(py); + if self.initial_gv == self.gv.get() { - return Ok(()); + return Ok(policy); } Err($crate::new_py_error!( diff --git a/src/pyclasses/cache.rs b/src/pyclasses/cache.rs index 131d364..a8a757a 100644 --- a/src/pyclasses/cache.rs +++ b/src/pyclasses/cache.rs @@ -556,7 +556,7 @@ impl PyCache { let initial_gv = gv.get(); let result = PyCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() @@ -574,7 +574,7 @@ impl PyCache { let initial_gv = gv.get(); let result = PyCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() @@ -591,7 +591,7 @@ impl PyCache { let initial_gv = gv.get(); let result = PyCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() @@ -698,14 +698,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, nopolicy::NoPolicy); #[pyo3::pymethods] impl $name { @@ -719,7 +719,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let mut iter = slf.iter.lock(); @@ -735,7 +735,7 @@ macro_rules! implement_iterator { /// Returns how many items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; Ok(slf.iter.lock().len()) } diff --git a/src/pyclasses/fifocache.rs b/src/pyclasses/fifocache.rs index da3a015..f76d625 100644 --- a/src/pyclasses/fifocache.rs +++ b/src/pyclasses/fifocache.rs @@ -559,7 +559,7 @@ impl PyFIFOCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(inner.policy_py_attached(slf.py()).iter()), gv, initial_gv, @@ -574,7 +574,7 @@ impl PyFIFOCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(inner.policy_py_attached(slf.py()).iter()), gv, initial_gv, @@ -589,7 +589,7 @@ impl PyFIFOCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(inner.policy_py_attached(slf.py()).iter()), gv, initial_gv, @@ -716,14 +716,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, fifopolicy::FIFOPolicy); #[pyo3::pymethods] impl $name { @@ -737,7 +737,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let mut iter = slf.iter.lock(); @@ -753,7 +753,7 @@ macro_rules! implement_iterator { /// Returns how many items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; Ok(slf.iter.lock().len()) } diff --git a/src/pyclasses/lfucache.rs b/src/pyclasses/lfucache.rs index 879ff69..1f4b6ce 100644 --- a/src/pyclasses/lfucache.rs +++ b/src/pyclasses/lfucache.rs @@ -579,7 +579,7 @@ impl PyLFUCache { let iter = policy.iter(gv); let result = PyLFUCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -595,7 +595,7 @@ impl PyLFUCache { let iter = policy.iter(gv); let result = PyLFUCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -611,7 +611,7 @@ impl PyLFUCache { let iter = policy.iter(gv); let result = PyLFUCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -634,7 +634,7 @@ impl PyLFUCache { let iter = policy.iter(gv); let result = PyLFUCacheItemsWithFrequency { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -783,14 +783,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, lfupolicy::LFUPolicy); #[pyo3::pymethods] impl $name { @@ -804,7 +804,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let mut iter = slf.iter.lock(); @@ -820,7 +820,7 @@ macro_rules! implement_iterator { /// Returns how many items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; Ok(slf.iter.lock().len()) } diff --git a/src/pyclasses/lrucache.rs b/src/pyclasses/lrucache.rs index 08541b0..4f5eb5c 100644 --- a/src/pyclasses/lrucache.rs +++ b/src/pyclasses/lrucache.rs @@ -586,7 +586,7 @@ impl PyLRUCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).list().iter() }), @@ -603,7 +603,7 @@ impl PyLRUCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).list().iter() }), @@ -620,7 +620,7 @@ impl PyLRUCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).list().iter() }), @@ -770,14 +770,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, lrupolicy::LRUPolicy); #[pyo3::pymethods] impl $name { @@ -791,7 +791,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let mut iter = slf.iter.lock(); @@ -807,7 +807,7 @@ macro_rules! implement_iterator { /// Returns how many items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; Ok(slf.iter.lock().len()) } diff --git a/src/pyclasses/rrcache.rs b/src/pyclasses/rrcache.rs index 3eb9984..60a8527 100644 --- a/src/pyclasses/rrcache.rs +++ b/src/pyclasses/rrcache.rs @@ -559,7 +559,7 @@ impl PyRRCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() }), @@ -576,7 +576,7 @@ impl PyRRCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() }), @@ -593,7 +593,7 @@ impl PyRRCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy_py_attached(slf.py()).table().iter() }), @@ -712,14 +712,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, rrpolicy::RRPolicy); #[pyo3::pymethods] impl $name { @@ -733,7 +733,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let mut iter = slf.iter.lock(); @@ -749,7 +749,7 @@ macro_rules! implement_iterator { /// Returns how many items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; Ok(slf.iter.lock().len()) } diff --git a/src/pyclasses/ttlcache.rs b/src/pyclasses/ttlcache.rs index 6c1b83f..f6d36a6 100644 --- a/src/pyclasses/ttlcache.rs +++ b/src/pyclasses/ttlcache.rs @@ -560,14 +560,18 @@ impl PyTTLCache { fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { let inner = slf.get().0.get()?; - let iter = inner.policy_py_attached(slf.py()).iter(inner.shared()); + // Create the iterator and read the generation under one lock, or a mutation in + // between would make stale positions look current. + let mut policy = inner.policy_py_attached(slf.py()); + let iter = policy.iter(inner.shared()); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); + drop(policy); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, @@ -578,14 +582,18 @@ impl PyTTLCache { fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { let inner = slf.get().0.get()?; - let iter = inner.policy_py_attached(slf.py()).iter(inner.shared()); + // Create the iterator and read the generation under one lock, or a mutation in + // between would make stale positions look current. + let mut policy = inner.policy_py_attached(slf.py()); + let iter = policy.iter(inner.shared()); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); + drop(policy); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, @@ -596,14 +604,18 @@ impl PyTTLCache { fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { let inner = slf.get().0.get()?; - let iter = inner.policy_py_attached(slf.py()).iter(inner.shared()); + // Create the iterator and read the generation under one lock, or a mutation in + // between would make stale positions look current. + let mut policy = inner.policy_py_attached(slf.py()); + let iter = policy.iter(inner.shared()); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); + drop(policy); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, @@ -802,14 +814,18 @@ impl PyTTLCache { ) -> pyo3::PyResult> { let inner = slf.get().0.get()?; - let iter = inner.policy_py_attached(slf.py()).iter(inner.shared()); + // Create the iterator and read the generation under one lock, or a mutation in + // between would make stale positions look current. + let mut policy = inner.policy_py_attached(slf.py()); + let iter = policy.iter(inner.shared()); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); + drop(policy); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheItemsWithExpire { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, @@ -854,14 +870,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, ttlpolicy::TTLPolicy); #[pyo3::pymethods] impl $name { @@ -875,7 +891,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let mut iter = slf.iter.lock(); @@ -895,7 +911,7 @@ macro_rules! implement_iterator { /// Returns how many not-expired items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let iter = slf.iter.lock().clone(); @@ -905,7 +921,7 @@ macro_rules! implement_iterator { /// Returns whether any not-expired item is left, without counting them all. fn __bool__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let mut iter = slf.iter.lock().clone(); diff --git a/src/pyclasses/vttlcache.rs b/src/pyclasses/vttlcache.rs index e96b9f8..e2d3584 100644 --- a/src/pyclasses/vttlcache.rs +++ b/src/pyclasses/vttlcache.rs @@ -539,7 +539,7 @@ impl PyVTTLCache { let iter = policy.iter(gv); let result = PyVTTLCacheItems { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -555,7 +555,7 @@ impl PyVTTLCache { let iter = policy.iter(gv); let result = PyVTTLCacheValues { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -571,7 +571,7 @@ impl PyVTTLCache { let iter = policy.iter(gv); let result = PyVTTLCacheKeys { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -769,7 +769,7 @@ impl PyVTTLCache { let iter = policy.iter(gv); let result = PyVTTLCacheItemsWithExpire { - cache: slf.as_any().clone().unbind(), + cache: slf.clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), @@ -816,14 +816,14 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { - cache: pyo3::Py, + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, } } - implement_view_guard!($name); + implement_view_guard!($name, vttlpolicy::VTTLPolicy); #[pyo3::pymethods] impl $name { @@ -837,7 +837,7 @@ macro_rules! implement_iterator { } fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let mut iter = slf.iter.lock(); @@ -857,7 +857,7 @@ macro_rules! implement_iterator { /// Returns how many not-expired items are left to yield. fn __len__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let iter = slf.iter.lock().clone(); @@ -867,7 +867,7 @@ macro_rules! implement_iterator { /// Returns whether any not-expired item is left, without counting them all. fn __bool__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - slf.check_generation()?; + let _policy = slf.lock_and_check(slf.py())?; let now = std::time::SystemTime::now(); let mut iter = slf.iter.lock().clone();