diff --git a/src/policies/wrapped.rs b/src/policies/wrapped.rs index 6208f41..3ec77b4 100644 --- a/src/policies/wrapped.rs +++ b/src/policies/wrapped.rs @@ -22,12 +22,16 @@ use crate::policies::traits::VacantExt; /// /// The shared (lock-free) fields of the policy are accessible directly via /// [`Wrapped::shared`], while mutable state is accessed through the inner -/// [`std::sync::Mutex`]. +/// [`parking_lot::Mutex`]. pub struct Wrapped { /// Read-only fields after initialization — no lock required. /// Accessible directly without acquiring the mutex. shared: P::Shared, - /// Mutable policy state — protected by a [`std::sync::Mutex`]. + /// Mutable policy state — protected by a [`parking_lot::Mutex`]. + /// + /// Wait for it with [`Self::policy_py_attached`] (or [`Self::try_policy`]), never + /// with a plain `lock()`: a thread blocked while attached deadlocks with a lock + /// holder that needs the interpreter (#95). inner: parking_lot::Mutex

, } @@ -46,14 +50,6 @@ impl Wrapped

{ &self.shared } - /// Acquires the mutex and returns a guard over the mutable policy state. - /// - /// Prefer [`Self::policy_py_attached`] when GIL is attached. - #[inline(always)] - pub fn policy(&self) -> parking_lot::MutexGuard<'_, P> { - self.inner.lock() - } - /// Acquires the mutex and returns a guard over the mutable policy state. // https://github.com/awolverp/cachebox/issues/95 #[inline(always)] @@ -115,8 +111,8 @@ fn insert_inner( impl Wrapped

{ /// Returns the remaining size. Equals to `maxsize - current_size`. #[inline] - pub fn remaining_size(&self) -> usize { - let policy = self.inner.lock(); + pub fn remaining_size(&self, py: pyo3::Python<'_>) -> usize { + let policy = self.policy_py_attached(py); self.shared.maxsize().saturating_sub(policy.current_size()) } diff --git a/src/pyclasses/cache.rs b/src/pyclasses/cache.rs index ef966f2..f0b9618 100644 --- a/src/pyclasses/cache.rs +++ b/src/pyclasses/cache.rs @@ -117,15 +117,15 @@ impl PyCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.policy().current_size()) + Ok(inner.policy_py_attached(py).current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -137,41 +137,41 @@ impl PyCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity()) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); FIXED_SIZE + (policy.table().capacity() * size_of::()) } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -191,19 +191,19 @@ impl PyCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -455,9 +455,9 @@ impl PyCache { /// Remove and return a (key, value) pair as a 2-tuple. /// /// NOTE: `Cache` always raises `NotImplementedError` because has neither policy nor algorithm to evict items. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -479,19 +479,19 @@ impl PyCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -668,9 +668,9 @@ impl PyCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { if let Ok(inner) = self.0.get() { - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } else { // object is not initialized, so there's nothing to clear diff --git a/src/pyclasses/fifocache.rs b/src/pyclasses/fifocache.rs index 1c062a8..87deb3c 100644 --- a/src/pyclasses/fifocache.rs +++ b/src/pyclasses/fifocache.rs @@ -123,15 +123,15 @@ impl PyFIFOCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.policy().current_size()) + Ok(inner.policy_py_attached(py).current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -143,31 +143,31 @@ impl PyFIFOCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity().min(policy.entries().capacity())) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); debug_assert!(policy.table().len() == policy.entries().len()); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); let table_cap = policy.table().capacity() * size_of::(); let vecdeque_cap = policy.entries().capacity() * size_of::(); @@ -175,11 +175,11 @@ impl PyFIFOCache { } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -199,19 +199,19 @@ impl PyFIFOCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -458,9 +458,9 @@ impl PyFIFOCache { } /// Remove and return a (key, value) pair as a 2-tuple. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -482,19 +482,19 @@ impl PyFIFOCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -692,12 +692,12 @@ impl PyFIFOCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/src/pyclasses/lfucache.rs b/src/pyclasses/lfucache.rs index 81f51da..a780463 100644 --- a/src/pyclasses/lfucache.rs +++ b/src/pyclasses/lfucache.rs @@ -139,15 +139,15 @@ impl PyLFUCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.policy().current_size()) + Ok(inner.policy_py_attached(py).current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -159,31 +159,31 @@ impl PyLFUCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity()) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); debug_assert!(policy.table().len() == policy.heap().len()); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); let table_cap = policy.table().capacity() * 8; let list_cap = policy.heap().len() * size_of::(); @@ -192,11 +192,11 @@ impl PyLFUCache { } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -216,19 +216,19 @@ impl PyLFUCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -478,9 +478,9 @@ impl PyLFUCache { } /// Remove and return a (key, value) pair as a 2-tuple. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -501,19 +501,19 @@ impl PyLFUCache { } /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -759,12 +759,12 @@ impl PyLFUCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/src/pyclasses/lrucache.rs b/src/pyclasses/lrucache.rs index 4a950b9..c5e0385 100644 --- a/src/pyclasses/lrucache.rs +++ b/src/pyclasses/lrucache.rs @@ -149,15 +149,15 @@ impl PyLRUCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.policy().current_size()) + Ok(inner.policy_py_attached(py).current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -169,31 +169,31 @@ impl PyLRUCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity().max(policy.list().capacity())) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); debug_assert!(policy.table().len() == policy.list().len()); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); let table_cap = policy.table().capacity() * 8; let list_cap = policy.list().len() * size_of::(); @@ -202,11 +202,11 @@ impl PyLRUCache { } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -226,19 +226,19 @@ impl PyLRUCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -485,9 +485,9 @@ impl PyLRUCache { } /// Remove and return a (key, value) pair as a 2-tuple. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -509,19 +509,19 @@ impl PyLRUCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -737,12 +737,12 @@ impl PyLRUCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/src/pyclasses/rrcache.rs b/src/pyclasses/rrcache.rs index 2112cb4..90b6085 100644 --- a/src/pyclasses/rrcache.rs +++ b/src/pyclasses/rrcache.rs @@ -121,15 +121,15 @@ impl PyRRCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.policy().current_size()) + Ok(inner.policy_py_attached(py).current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -141,40 +141,40 @@ impl PyRRCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity()) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); FIXED_SIZE + policy.table().capacity() * size_of::() } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -194,19 +194,19 @@ impl PyRRCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -458,9 +458,9 @@ impl PyRRCache { /// Remove and return a (key, value) pair as a 2-tuple. /// /// NOTE: `Cache` always raises `NotImplementedError` because has neither policy nor algorithm to evict items. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -482,19 +482,19 @@ impl PyRRCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -682,12 +682,12 @@ impl PyRRCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/src/pyclasses/ttlcache.rs b/src/pyclasses/ttlcache.rs index aa69c51..b95e5f5 100644 --- a/src/pyclasses/ttlcache.rs +++ b/src/pyclasses/ttlcache.rs @@ -102,21 +102,21 @@ impl PyTTLCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(inner.shared().generation_version()); Ok(policy.current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; { - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(inner.shared().generation_version()); } - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -135,31 +135,31 @@ impl PyTTLCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity().min(policy.entries().capacity())) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); debug_assert!(policy.table().len() == policy.entries().len()); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); let table_cap = policy.table().capacity() * size_of::(); let vecdeque_cap = policy.entries().capacity() * size_of::(); @@ -168,11 +168,11 @@ impl PyTTLCache { } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -192,19 +192,19 @@ impl PyTTLCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -463,9 +463,9 @@ impl PyTTLCache { } /// Remove and return a (key, value) pair as a 2-tuple. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -487,19 +487,19 @@ impl PyTTLCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -661,10 +661,10 @@ impl PyTTLCache { #[inline] #[pyo3(signature=(*, reuse=false))] - fn expire(&self, reuse: bool) -> pyo3::PyResult<()> { + fn expire(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(shared.generation_version()); @@ -772,9 +772,12 @@ impl PyTTLCache { } } - fn popitem_with_expire(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject, f64)> { + fn popitem_with_expire( + &self, + py: pyo3::Python, + ) -> pyo3::PyResult<(alias::PyObject, alias::PyObject, f64)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -823,12 +826,12 @@ impl PyTTLCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/src/pyclasses/vttlcache.rs b/src/pyclasses/vttlcache.rs index f1e86c7..c776153 100644 --- a/src/pyclasses/vttlcache.rs +++ b/src/pyclasses/vttlcache.rs @@ -96,22 +96,22 @@ impl PyVTTLCache { } #[inline] - fn current_size(&self) -> pyo3::PyResult { + fn current_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(inner.shared().generation_version()); Ok(policy.current_size()) } #[inline] - fn remaining_size(&self) -> pyo3::PyResult { + fn remaining_size(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; { - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(inner.shared().generation_version()); } - Ok(inner.remaining_size()) + Ok(inner.remaining_size(py)) } #[getter] @@ -123,31 +123,31 @@ impl PyVTTLCache { /// Returns the number of elements the map can hold without reallocating. #[inline] - fn capacity(&self) -> pyo3::PyResult { + fn capacity(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().capacity()) } /// Returns the number of entries currently in the cache. #[inline] - fn __len__(&self) -> pyo3::PyResult { + fn __len__(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); debug_assert!(policy.table().len() == policy.heap().len()); Ok(policy.table().len()) } #[inline] - fn __sizeof__(&self) -> usize { + fn __sizeof__(&self, py: pyo3::Python) -> usize { const FIXED_SIZE: usize = size_of::>(); let Ok(inner) = self.0.get() else { return FIXED_SIZE; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); let table_cap = policy.table().capacity() * 8; let list_cap = policy.heap().len() * size_of::(); @@ -156,11 +156,11 @@ impl PyVTTLCache { } #[inline] - fn __bool__(&self) -> bool { + fn __bool__(&self, py: pyo3::Python) -> bool { let Ok(inner) = self.0.get() else { return false; }; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); !policy.table().is_empty() } @@ -180,19 +180,19 @@ impl PyVTTLCache { /// Returns `True` if cache is empty. #[inline] - fn is_empty(&self) -> pyo3::PyResult { + fn is_empty(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.table().is_empty()) } /// Returns `True` when the cumulative size has reached the maxsize limit. #[inline] - fn is_full(&self) -> pyo3::PyResult { + fn is_full(&self, py: pyo3::Python) -> pyo3::PyResult { let inner = self.0.get()?; let shared = inner.shared(); - let policy = inner.policy(); + let policy = inner.policy_py_attached(py); Ok(policy.current_size() >= shared.maxsize()) } @@ -437,9 +437,9 @@ impl PyVTTLCache { } /// Remove and return a (key, value) pair as a 2-tuple. - fn popitem(&self) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { + fn popitem(&self, py: pyo3::Python) -> pyo3::PyResult<(alias::PyObject, alias::PyObject)> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); let handle = policy.evict(inner.shared())?; drop(policy); @@ -461,19 +461,19 @@ impl PyVTTLCache { /// Shrinks the internal allocation as close to the current length as possible. #[inline] - fn shrink_to_fit(&self) -> pyo3::PyResult<()> { + fn shrink_to_fit(&self, py: pyo3::Python) -> pyo3::PyResult<()> { let inner = self.0.get()?; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.shrink_to_fit(inner.shared()); Ok(()) } /// Removes all entries from the table and resets the cumulative size to zero. #[pyo3(signature=(*, reuse=false))] - fn clear(&self, reuse: bool) -> pyo3::PyResult<()> { + fn clear(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(shared); @@ -641,10 +641,10 @@ impl PyVTTLCache { #[inline] #[pyo3(signature=(*, reuse=false))] - fn expire(&self, reuse: bool) -> pyo3::PyResult<()> { + fn expire(&self, py: pyo3::Python, reuse: bool) -> pyo3::PyResult<()> { let inner = self.0.get()?; let shared = inner.shared(); - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.expire(shared.generation_version()); @@ -791,12 +791,12 @@ impl PyVTTLCache { Ok(()) } - fn __clear__(&self) { + fn __clear__(&self, py: pyo3::Python) { let Ok(inner) = self.0.get() else { // object is not initialized, so there's nothing to clear return; }; - let mut policy = inner.policy(); + let mut policy = inner.policy_py_attached(py); policy.clear(inner.shared()); } } diff --git a/tests/mixins.py b/tests/mixins.py index 811d918..19907e5 100644 --- a/tests/mixins.py +++ b/tests/mixins.py @@ -1142,6 +1142,85 @@ def insert(base): print("finished") """ +LOCK_WAITERS_WITH_FINALIZERS = """ +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) + +# a hang prints every thread's stack before the parent's timeout +faulthandler.dump_traceback_later(50, exit=True) +# switch threads as often as possible, also in the middle of a finalizer +sys.setswitchinterval(1e-6) + +errors = [] +threading.excepthook = lambda args: errors.append(repr(args.exc_value)) + + +class Value: + def __del__(self): + # runs while clear() or an eviction holds the cache lock + self.dropped = True + + +# every method that waits for the cache lock without an argument +methods = [ + getattr(cache, m) + for m in ( + "__len__", "__bool__", "__sizeof__", "capacity", "current_size", + "is_empty", "is_full", "shrink_to_fit", "popitem", "clear", + ) +] +methods += [ + getattr(cache, m) + for m in ("remaining_size", "expire", "popitem_with_expire") + if hasattr(cache, m) +] +stop = threading.Event() + + +def worker(seed): + rng = random.Random(seed) + while not stop.is_set(): + if rng.random() < 0.6: + cache[rng.randrange(2000)] = Value() + else: + try: + rng.choice(methods)() + except (KeyError, OverflowError): + pass # empty cache, or Cache has nothing to pop + + +deadline = time.monotonic() + 3 +started = 0 +while time.monotonic() < deadline: + # starting threads stops the world on free-threaded builds + batch = [threading.Thread(target=worker, args=(started + i,)) for i in range(8)] + for t in batch: + t.start() + started += len(batch) + time.sleep(0.1) + stop.set() + for t in batch: + t.join() + stop.clear() + +assert not errors, errors[:3] +print("ok") +""" + class IssuesMixin(BaseMixin): def test_issue_5(self): @@ -1199,6 +1278,30 @@ def test_concurrent_inserts_with_finalizers(self): done.stderr or f"exit code {done.returncode}" ) + def test_lock_waiters_with_finalizers(self): + # Follow-up to #95: methods such as len() and clear() waited for the + # cache lock without detaching. With the GIL, a finalizer running under + # clear() hands the GIL to a thread blocked there; on free-threaded + # builds the blocked thread holds up a stop-the-world. + name = type(self.create_cache()).__name__ + + try: + done = subprocess.run( + [sys.executable, "-c", LOCK_WAITERS_WITH_FINALIZERS, name], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + except subprocess.TimeoutExpired: + pytest.fail( + f"{name}: threads waiting for the cache lock never returned" + ) + + assert done.stdout.strip() == "ok", ( + done.stderr or f"exit code {done.returncode}" + ) + def test_none_refcount(self): cache = self.create_cache(1000)