cachebox 6.2.9__tar.gz → 6.2.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cachebox-6.2.9 → cachebox-6.2.10}/Cargo.lock +1 -1
- {cachebox-6.2.9 → cachebox-6.2.10}/Cargo.toml +1 -1
- {cachebox-6.2.9 → cachebox-6.2.10}/PKG-INFO +1 -1
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/algorithms.md +17 -9
- {cachebox-6.2.9 → cachebox-6.2.10}/pyproject.toml +2 -2
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/linked_list.rs +80 -50
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/utils.rs +13 -3
- {cachebox-6.2.9 → cachebox-6.2.10}/src/macro_rules.rs +1 -1
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/fifopolicy.rs +5 -4
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/ttlpolicy.rs +9 -9
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/vttlpolicy.rs +5 -4
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/wrapped.rs +120 -37
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/cache.rs +35 -37
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/fifocache.rs +37 -39
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/lfucache.rs +38 -40
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/lrucache.rs +38 -40
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/rrcache.rs +36 -38
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/ttlcache.rs +47 -46
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/vttlcache.rs +41 -43
- {cachebox-6.2.9 → cachebox-6.2.10}/src/typeref.rs +6 -3
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/mixins.py +320 -0
- cachebox-6.2.10/tests/test_hashing.py +156 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_impls.py +33 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/uv.lock +28 -28
- cachebox-6.2.9/tests/test_hashing.py +0 -81
- {cachebox-6.2.9 → cachebox-6.2.10}/.github/dependabot.yaml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/CI.yml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/docs.yml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/python-test.yml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/.gitignore +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/AGENTS.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/LICENSE +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/LICENSE-THIRD-PARTY +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/README.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/build.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/__init__.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_cachebox.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_core.pyi +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_wrappers.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/py.typed +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/utils.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/impls.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/index.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/utils.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/faq.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/getting-started.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/index.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/installation.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/migration.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/tips.md +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/docs/mkdocs.yml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/rust-toolchain.toml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/rustfmt.toml +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/alloc.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/bitmask.rs +2 -2
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/generic.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/lsx.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/mod.rs +2 -2
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/neon.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/sse2.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/mod.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/tag.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/mod.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/raw.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/scopeguard.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/util.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/alias.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/lazyheap.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/mod.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/onceinit.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/pickle.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/lib.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/common.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/lfupolicy.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/lrupolicy.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/mod.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/nopolicy.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/rrpolicy.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/traits.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/base.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/mod.rs +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/__init__.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/conftest.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_policy_model.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_utils.py +0 -0
- {cachebox-6.2.9 → cachebox-6.2.10}/typos.toml +0 -0
|
@@ -15,7 +15,8 @@ in **when** and **which** entries are removed under pressure (or over time).
|
|
|
15
15
|
| [`TTLCache`](#ttlcache) | Global TTL + FIFO among live entries | Uniform freshness windows | Per-key TTLs needed |
|
|
16
16
|
| [`VTTLCache`](#vttlcache) | Per-item TTL (optional non-expiring) | Mixed lifetimes | All items share one TTL |
|
|
17
17
|
|
|
18
|
-
Complexity (
|
|
18
|
+
Complexity (expected, as for any hash table; amortized where marked with `~`; `e` is the number of
|
|
19
|
+
expired entries the call removes):
|
|
19
20
|
|
|
20
21
|
| Class | get | insert | delete | popitem |
|
|
21
22
|
| ----------- | ---- | -------------- | -------------- | ------------- |
|
|
@@ -24,9 +25,13 @@ Complexity (worst-case, amortized where marked with `~`):
|
|
|
24
25
|
| `RRCache` | O(1) | O(1)~ | O(1) | O(1) expected |
|
|
25
26
|
| `LRUCache` | O(1) | O(1)~ | O(1) | O(1) |
|
|
26
27
|
| `LFUCache` | O(1) | O(log n)~ | O(log n) | O(log n)~ |
|
|
27
|
-
| `TTLCache` | O(1) | O(e)~
|
|
28
|
+
| `TTLCache` | O(1) | O(e+1)~ | O(n) | O(1)~ |
|
|
28
29
|
| `VTTLCache` | O(1) | O((e+1) log n) | O((e+1) log n) | O(log n)~ |
|
|
29
30
|
|
|
31
|
+
`LFUCache.get` only bumps a counter; the heap is repaired on the next eviction, so a `popitem`
|
|
32
|
+
after `k` reads takes O(k log n), which amortizes to O(log n) per read. Iterating an `LFUCache` or
|
|
33
|
+
a `VTTLCache` re-sorts all entries, O(n log n), when anything changed since the last iteration.
|
|
34
|
+
|
|
30
35
|
## Decision Guide
|
|
31
36
|
|
|
32
37
|
```
|
|
@@ -56,7 +61,7 @@ print(cache["key"]) # value
|
|
|
56
61
|
|
|
57
62
|
# Overflow when full
|
|
58
63
|
cache.update({i: i for i in range(200)})
|
|
59
|
-
# OverflowError: The cache has
|
|
64
|
+
# OverflowError: The cache has no algorithm to evict items
|
|
60
65
|
|
|
61
66
|
# popitem always fails — there is no eviction order
|
|
62
67
|
cache.popitem() # OverflowError
|
|
@@ -138,7 +143,7 @@ print(cache.least_recently_used()) # still 2 if nothing else was accessed
|
|
|
138
143
|
## LFUCache
|
|
139
144
|
|
|
140
145
|
Least-Frequently-Used. The key with the lowest access count is evicted first. Ties are broken by
|
|
141
|
-
|
|
146
|
+
insertion order (inserted earlier first).
|
|
142
147
|
|
|
143
148
|
```python
|
|
144
149
|
from cachebox import LFUCache
|
|
@@ -208,9 +213,10 @@ cache3.stop_sweeper() # stop the daemon thread when done
|
|
|
208
213
|
|
|
209
214
|
!!! note "Lazy expiry"
|
|
210
215
|
|
|
211
|
-
Without `sweep_interval`, expired entries
|
|
212
|
-
(`
|
|
213
|
-
|
|
216
|
+
Without `sweep_interval`, expired entries are misses for `get` and `[]` but stay in the
|
|
217
|
+
cache, and `len()` / `bool()` still count them, until an insert, iteration, `current_size()`
|
|
218
|
+
or `expire()` removes them. See
|
|
219
|
+
[TTL sweepers](tips.md#ttlcache-vttlcache-background-thread).
|
|
214
220
|
|
|
215
221
|
---
|
|
216
222
|
|
|
@@ -227,7 +233,8 @@ import time
|
|
|
227
233
|
# ttl= here applies only to the initial iterable, not as a global default
|
|
228
234
|
cache = VTTLCache(100, iterable={i: i for i in range(4)}, ttl=3)
|
|
229
235
|
time.sleep(3)
|
|
230
|
-
|
|
236
|
+
cache.expire()
|
|
237
|
+
print(len(cache)) # 0
|
|
231
238
|
|
|
232
239
|
cache.insert("session", "tok", ttl=5) # lives 5 seconds
|
|
233
240
|
cache.insert("config", {"theme": "dark"}) # never expires
|
|
@@ -275,6 +282,7 @@ Regardless of policy:
|
|
|
275
282
|
- **`getsizeof`** — optional weighted capacity (see [Getting Started](getting-started.md#capacity-size-and-weighted-entries)).
|
|
276
283
|
- **`insert` preferred over `[]=`** when you need the previous value or cross-policy consistency.
|
|
277
284
|
- **Iterators are one-shot** — not live dict views; modifying the cache while iterating raises
|
|
278
|
-
`RuntimeError`.
|
|
285
|
+
`RuntimeError`. In an `LRUCache`, `get` and `[]` count as modifications (they reorder entries);
|
|
286
|
+
in an `LFUCache`, starting a new iteration after a read invalidates iterators already open.
|
|
279
287
|
|
|
280
288
|
Full method lists: [API Reference — Classes](api/impls.md).
|
|
@@ -55,8 +55,8 @@ lint = [
|
|
|
55
55
|
"ruff>=0.16.9",
|
|
56
56
|
]
|
|
57
57
|
maturin = [
|
|
58
|
-
"maturin == 1.
|
|
59
|
-
"maturin[patchelf] == 1.
|
|
58
|
+
"maturin == 1.15.0 ; sys_platform != 'linux'",
|
|
59
|
+
"maturin[patchelf] == 1.15.0 ; sys_platform == 'linux'",
|
|
60
60
|
]
|
|
61
61
|
test = [
|
|
62
62
|
"hypothesis >= 6.165.0",
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
use std::alloc::Layout;
|
|
1
2
|
use std::alloc::alloc;
|
|
2
3
|
use std::alloc::dealloc;
|
|
3
4
|
use std::alloc::handle_alloc_error;
|
|
4
|
-
use std::alloc::Layout;
|
|
5
5
|
use std::marker::PhantomData;
|
|
6
|
-
use std::
|
|
7
|
-
use std::ptr::NonNull;
|
|
8
|
-
use std::ptr::{self};
|
|
6
|
+
use std::ptr;
|
|
9
7
|
|
|
10
8
|
/// Maximum number of nodes parked on the free list
|
|
11
9
|
const FREE_LIST_CAPACITY: usize = 256;
|
|
@@ -42,7 +40,7 @@ pub struct Node<T> {
|
|
|
42
40
|
|
|
43
41
|
/// A doubly-linked list with an internal, bounded free-list for node reuse.
|
|
44
42
|
pub struct LinkedList<T> {
|
|
45
|
-
sentinel: NonNull<Links>,
|
|
43
|
+
sentinel: ptr::NonNull<Links>,
|
|
46
44
|
free_head: *mut Links,
|
|
47
45
|
free_len: usize,
|
|
48
46
|
len: usize,
|
|
@@ -61,7 +59,7 @@ impl<T> LinkedList<T> {
|
|
|
61
59
|
/// - The caller must not use `node` again as an "unlinked" node until it is
|
|
62
60
|
/// unlinked again.
|
|
63
61
|
#[inline]
|
|
64
|
-
unsafe fn push_front_node(&mut self, node: NonNull<Links>) {
|
|
62
|
+
unsafe fn push_front_node(&mut self, node: ptr::NonNull<Links>) {
|
|
65
63
|
let s = self.sentinel.as_ptr();
|
|
66
64
|
let n = node.as_ptr();
|
|
67
65
|
unsafe {
|
|
@@ -80,7 +78,7 @@ impl<T> LinkedList<T> {
|
|
|
80
78
|
///
|
|
81
79
|
/// Same contract as [`push_front_node`](Self::push_front_node).
|
|
82
80
|
#[inline]
|
|
83
|
-
unsafe fn push_back_node(&mut self, node: NonNull<Links>) {
|
|
81
|
+
unsafe fn push_back_node(&mut self, node: ptr::NonNull<Links>) {
|
|
84
82
|
let s = self.sentinel.as_ptr();
|
|
85
83
|
let n = node.as_ptr();
|
|
86
84
|
unsafe {
|
|
@@ -95,7 +93,7 @@ impl<T> LinkedList<T> {
|
|
|
95
93
|
|
|
96
94
|
/// Removes and returns the node at the front of the list, if any.
|
|
97
95
|
#[inline]
|
|
98
|
-
fn pop_front_node(&mut self) -> Option<NonNull<Links>> {
|
|
96
|
+
fn pop_front_node(&mut self) -> Option<ptr::NonNull<Links>> {
|
|
99
97
|
if self.len == 0 {
|
|
100
98
|
return None;
|
|
101
99
|
}
|
|
@@ -110,12 +108,12 @@ impl<T> LinkedList<T> {
|
|
|
110
108
|
};
|
|
111
109
|
self.len -= 1;
|
|
112
110
|
// SAFETY: read from a valid node above, so non-null.
|
|
113
|
-
Some(unsafe { NonNull::new_unchecked(node) })
|
|
111
|
+
Some(unsafe { ptr::NonNull::new_unchecked(node) })
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
/// Removes and returns the node at the back of the list, if any.
|
|
117
115
|
#[inline]
|
|
118
|
-
fn pop_back_node(&mut self) -> Option<NonNull<Links>> {
|
|
116
|
+
fn pop_back_node(&mut self) -> Option<ptr::NonNull<Links>> {
|
|
119
117
|
if self.len == 0 {
|
|
120
118
|
return None;
|
|
121
119
|
}
|
|
@@ -130,7 +128,7 @@ impl<T> LinkedList<T> {
|
|
|
130
128
|
};
|
|
131
129
|
self.len -= 1;
|
|
132
130
|
// SAFETY: read from a valid node above, so non-null.
|
|
133
|
-
Some(unsafe { NonNull::new_unchecked(node) })
|
|
131
|
+
Some(unsafe { ptr::NonNull::new_unchecked(node) })
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
/// Unlinks `node` from the list, without deallocating or reading its element.
|
|
@@ -139,7 +137,7 @@ impl<T> LinkedList<T> {
|
|
|
139
137
|
///
|
|
140
138
|
/// `node` must point to a node currently linked into this list (not the sentinel).
|
|
141
139
|
#[inline]
|
|
142
|
-
unsafe fn unlink_node(&mut self, node: NonNull<Links>) {
|
|
140
|
+
unsafe fn unlink_node(&mut self, node: ptr::NonNull<Links>) {
|
|
143
141
|
let n = node.as_ptr();
|
|
144
142
|
unsafe {
|
|
145
143
|
// SAFETY: `n` linked per fn contract; `prev`/`next` valid.
|
|
@@ -182,7 +180,7 @@ impl<T> LinkedList<T> {
|
|
|
182
180
|
/// `node` must point to a node whose `element` is initialized and which is
|
|
183
181
|
/// no longer linked into the list (already unlinked by the caller).
|
|
184
182
|
#[inline]
|
|
185
|
-
unsafe fn take_element_and_recycle(&mut self, node: NonNull<Links>) -> T {
|
|
183
|
+
unsafe fn take_element_and_recycle(&mut self, node: ptr::NonNull<Links>) -> T {
|
|
186
184
|
let node_ptr = node.as_ptr() as *mut Node<T>;
|
|
187
185
|
// SAFETY: `element` initialized per fn contract; `ptr::read` doesn't drop.
|
|
188
186
|
let element = unsafe { ptr::read(&(*node_ptr).element) };
|
|
@@ -197,7 +195,7 @@ impl<T> LinkedList<T> {
|
|
|
197
195
|
///
|
|
198
196
|
/// `node` must point to a node currently linked into this list
|
|
199
197
|
#[inline]
|
|
200
|
-
unsafe fn remove_node(&mut self, node: NonNull<Links>) -> T {
|
|
198
|
+
unsafe fn remove_node(&mut self, node: ptr::NonNull<Links>) -> T {
|
|
201
199
|
// SAFETY: caller guarantees `node` is linked.
|
|
202
200
|
unsafe { self.unlink_node(node) };
|
|
203
201
|
self.len -= 1;
|
|
@@ -212,20 +210,23 @@ impl<T> LinkedList<T> {
|
|
|
212
210
|
fn drop_nodes(&mut self) {
|
|
213
211
|
let s = self.sentinel.as_ptr();
|
|
214
212
|
while self.len != 0 {
|
|
215
|
-
unsafe {
|
|
213
|
+
let element = unsafe {
|
|
216
214
|
// SAFETY: `self.len != 0`, so `(*s).next` points to a real node.
|
|
217
215
|
let node = (*s).next;
|
|
218
216
|
let second = (*node).next;
|
|
219
217
|
(*second).prev = s;
|
|
220
218
|
(*s).next = second;
|
|
221
219
|
|
|
222
|
-
let node_ptr = node as *mut Node<T>;
|
|
223
220
|
// SAFETY: layout-compatible (see `Node` docs); `element` still init.
|
|
224
|
-
ptr::
|
|
225
|
-
// SAFETY: element
|
|
221
|
+
let element = ptr::read(&(*(node as *mut Node<T>)).element);
|
|
222
|
+
// SAFETY: element moved out above; node already unlinked.
|
|
226
223
|
self.recycle_node(node);
|
|
227
|
-
|
|
224
|
+
element
|
|
225
|
+
};
|
|
228
226
|
self.len -= 1;
|
|
227
|
+
// Dropped last: if this panics, the list is already consistent and
|
|
228
|
+
// the drop guard can carry on.
|
|
229
|
+
drop(element);
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -302,7 +303,7 @@ impl<T> LinkedList<T> {
|
|
|
302
303
|
p
|
|
303
304
|
};
|
|
304
305
|
// SAFETY: non-null checked above (or diverged).
|
|
305
|
-
let sentinel = unsafe { NonNull::new_unchecked(raw) };
|
|
306
|
+
let sentinel = unsafe { ptr::NonNull::new_unchecked(raw) };
|
|
306
307
|
LinkedList {
|
|
307
308
|
sentinel,
|
|
308
309
|
free_head: ptr::null_mut(),
|
|
@@ -348,7 +349,7 @@ impl<T> LinkedList<T> {
|
|
|
348
349
|
let node = unsafe { (*s).next };
|
|
349
350
|
// SAFETY: non-null above; layout-compatible (see `Node` docs).
|
|
350
351
|
Some(Cursor(unsafe {
|
|
351
|
-
NonNull::new_unchecked(node as *mut Node<T>)
|
|
352
|
+
ptr::NonNull::new_unchecked(node as *mut Node<T>)
|
|
352
353
|
}))
|
|
353
354
|
}
|
|
354
355
|
|
|
@@ -364,7 +365,7 @@ impl<T> LinkedList<T> {
|
|
|
364
365
|
let node = unsafe { (*s).prev };
|
|
365
366
|
// SAFETY: same as `cursor_front`.
|
|
366
367
|
Some(Cursor(unsafe {
|
|
367
|
-
NonNull::new_unchecked(node as *mut Node<T>)
|
|
368
|
+
ptr::NonNull::new_unchecked(node as *mut Node<T>)
|
|
368
369
|
}))
|
|
369
370
|
}
|
|
370
371
|
|
|
@@ -373,13 +374,13 @@ impl<T> LinkedList<T> {
|
|
|
373
374
|
pub fn push_front(&mut self, elt: T) -> Cursor<T> {
|
|
374
375
|
let node = self.get_node(elt);
|
|
375
376
|
// SAFETY: `get_node` never returns null.
|
|
376
|
-
let links = unsafe { NonNull::new_unchecked(node as *mut Links) };
|
|
377
|
+
let links = unsafe { ptr::NonNull::new_unchecked(node as *mut Links) };
|
|
377
378
|
|
|
378
379
|
// SAFETY: freshly obtained node, unlinked (see `push_front_node` contract).
|
|
379
380
|
unsafe { self.push_front_node(links) };
|
|
380
381
|
self.len += 1;
|
|
381
382
|
// SAFETY: same pointer validated above.
|
|
382
|
-
Cursor(unsafe { NonNull::new_unchecked(node) })
|
|
383
|
+
Cursor(unsafe { ptr::NonNull::new_unchecked(node) })
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
/// Removes and returns the front element, or `None` if the list is empty.
|
|
@@ -397,14 +398,14 @@ impl<T> LinkedList<T> {
|
|
|
397
398
|
pub fn push_back(&mut self, elt: T) -> Cursor<T> {
|
|
398
399
|
let node = self.get_node(elt);
|
|
399
400
|
// SAFETY: `get_node` always returns a non-null, freshly-usable `Node<T>` pointer.
|
|
400
|
-
let links = unsafe { NonNull::new_unchecked(node as *mut Links) };
|
|
401
|
+
let links = unsafe { ptr::NonNull::new_unchecked(node as *mut Links) };
|
|
401
402
|
|
|
402
403
|
// SAFETY: `links` refers to a node that was just obtained and is
|
|
403
404
|
// not linked into any list yet.
|
|
404
405
|
unsafe { self.push_back_node(links) };
|
|
405
406
|
self.len += 1;
|
|
406
407
|
// SAFETY: `node` is the same non-null pointer validated above.
|
|
407
|
-
Cursor(unsafe { NonNull::new_unchecked(node) })
|
|
408
|
+
Cursor(unsafe { ptr::NonNull::new_unchecked(node) })
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
/// Removes and returns the back element, or `None` if the list is empty.
|
|
@@ -439,17 +440,22 @@ impl<T> LinkedList<T> {
|
|
|
439
440
|
}
|
|
440
441
|
}
|
|
441
442
|
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
443
|
+
// Frees the rest of the list when it goes out of scope, which also covers an
|
|
444
|
+
// element destructor panicking in `LinkedList::drop`: the remaining elements,
|
|
445
|
+
// the free list and the sentinel are freed either way.
|
|
445
446
|
struct DropGuard<'a, T>(&'a mut LinkedList<T>);
|
|
446
447
|
|
|
447
448
|
impl<'a, T> Drop for DropGuard<'a, T> {
|
|
448
449
|
fn drop(&mut self) {
|
|
449
450
|
self.0.drop_nodes();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
unsafe {
|
|
452
|
+
// SAFETY: the list is being destroyed; no other references to the
|
|
453
|
+
// free list exist, satisfying `drop_freelist`'s contract.
|
|
454
|
+
self.0.drop_freelist();
|
|
455
|
+
// SAFETY: `sentinel` was allocated in `new` with exactly this
|
|
456
|
+
// layout and is never dereferenced after this point.
|
|
457
|
+
dealloc(self.0.sentinel.as_ptr() as *mut u8, Layout::new::<Links>());
|
|
458
|
+
}
|
|
453
459
|
}
|
|
454
460
|
}
|
|
455
461
|
|
|
@@ -457,16 +463,7 @@ impl<T> Drop for LinkedList<T> {
|
|
|
457
463
|
fn drop(&mut self) {
|
|
458
464
|
let guard = DropGuard(self);
|
|
459
465
|
guard.0.drop_nodes();
|
|
460
|
-
//
|
|
461
|
-
// only touched here and then never again (guard is forgotten next).
|
|
462
|
-
unsafe { guard.0.drop_freelist() };
|
|
463
|
-
mem::forget(guard);
|
|
464
|
-
// SAFETY: `self.sentinel` was allocated in `new` with exactly this
|
|
465
|
-
// layout (`Layout::new::<Links>()`), and after this point `self` is
|
|
466
|
-
// being destroyed, so the pointer is never dereferenced again.
|
|
467
|
-
unsafe {
|
|
468
|
-
dealloc(self.sentinel.as_ptr() as *mut u8, Layout::new::<Links>());
|
|
469
|
-
}
|
|
466
|
+
// `guard` drops here and frees the free list and the sentinel.
|
|
470
467
|
}
|
|
471
468
|
}
|
|
472
469
|
|
|
@@ -478,7 +475,7 @@ impl<T> Drop for LinkedList<T> {
|
|
|
478
475
|
/// later access or reposition that node via [`Cursor::element`],
|
|
479
476
|
/// [`Cursor::move_to_front`], [`Cursor::move_to_back`], or [`Cursor::unlink`].
|
|
480
477
|
///
|
|
481
|
-
/// `#[repr(transparent)]` over `NonNull<Node<T>>` means a `Cursor<T>` has the
|
|
478
|
+
/// `#[repr(transparent)]` over `ptr::NonNull<Node<T>>` means a `Cursor<T>` has the
|
|
482
479
|
/// exact same layout as the raw pointer it wraps — no extra state is tracked,
|
|
483
480
|
/// so the cursor does *not* know which `LinkedList` it came from, whether the
|
|
484
481
|
/// node is still linked, or whether other `Cursor`s alias the same node. All
|
|
@@ -489,9 +486,9 @@ impl<T> Drop for LinkedList<T> {
|
|
|
489
486
|
/// equality (`PartialEq`/`Eq`) compares the underlying pointer, i.e. identity
|
|
490
487
|
/// of the node, not the value of the element.
|
|
491
488
|
#[repr(transparent)]
|
|
492
|
-
pub struct Cursor<T>(NonNull<Node<T>>);
|
|
489
|
+
pub struct Cursor<T>(ptr::NonNull<Node<T>>);
|
|
493
490
|
|
|
494
|
-
// `NonNull<Node<T>>` is just a pointer; copying it is always safe.
|
|
491
|
+
// `ptr::NonNull<Node<T>>` is just a pointer; copying it is always safe.
|
|
495
492
|
impl<T> Clone for Cursor<T> {
|
|
496
493
|
#[inline]
|
|
497
494
|
fn clone(&self) -> Self {
|
|
@@ -516,10 +513,10 @@ impl<T> Cursor<T> {
|
|
|
516
513
|
/// Relies on `#[repr(C)]` on `Node<T>` placing `links` at offset 0, so the
|
|
517
514
|
/// cast is always valid regardless of `T`.
|
|
518
515
|
#[inline]
|
|
519
|
-
fn links(&self) -> NonNull<Links> {
|
|
516
|
+
fn links(&self) -> ptr::NonNull<Links> {
|
|
520
517
|
let ptr = self.0.as_ptr() as *mut Links;
|
|
521
|
-
// SAFETY: `self.0` is `NonNull`, so the reinterpreted pointer is non-null too.
|
|
522
|
-
unsafe { NonNull::new_unchecked(ptr) }
|
|
518
|
+
// SAFETY: `self.0` is `ptr::NonNull`, so the reinterpreted pointer is non-null too.
|
|
519
|
+
unsafe { ptr::NonNull::new_unchecked(ptr) }
|
|
523
520
|
}
|
|
524
521
|
|
|
525
522
|
/// Returns a reference to the node's element, with a caller-chosen lifetime.
|
|
@@ -621,7 +618,7 @@ impl<T> Cursor<T> {
|
|
|
621
618
|
pub struct RawIter<T> {
|
|
622
619
|
next: *mut Links,
|
|
623
620
|
len: usize,
|
|
624
|
-
_marker: PhantomData<NonNull<Node<T>>>,
|
|
621
|
+
_marker: PhantomData<ptr::NonNull<Node<T>>>,
|
|
625
622
|
}
|
|
626
623
|
|
|
627
624
|
impl<T> Iterator for RawIter<T> {
|
|
@@ -641,7 +638,7 @@ impl<T> Iterator for RawIter<T> {
|
|
|
641
638
|
self.next = unsafe { (*node).next };
|
|
642
639
|
// SAFETY: `node` is non-null (came from a valid linked `Links`), and
|
|
643
640
|
// by `#[repr(C)]` on `Node<T>` a `*mut Links` is a valid `*mut Node<T>`.
|
|
644
|
-
let cursor = Cursor(unsafe { NonNull::new_unchecked(node as *mut Node<T>) });
|
|
641
|
+
let cursor = Cursor(unsafe { ptr::NonNull::new_unchecked(node as *mut Node<T>) });
|
|
645
642
|
Some(cursor)
|
|
646
643
|
}
|
|
647
644
|
|
|
@@ -659,3 +656,36 @@ unsafe impl<T: Send + Send> Send for RawIter<T> {}
|
|
|
659
656
|
unsafe impl<T: Sync + Sync> Sync for RawIter<T> {}
|
|
660
657
|
unsafe impl<T: Send + Send> Send for Cursor<T> {}
|
|
661
658
|
unsafe impl<T: Sync + Sync> Sync for Cursor<T> {}
|
|
659
|
+
|
|
660
|
+
#[cfg(test)]
|
|
661
|
+
mod tests {
|
|
662
|
+
use super::LinkedList;
|
|
663
|
+
|
|
664
|
+
#[test]
|
|
665
|
+
fn a_panicking_element_drop_still_frees_every_node() {
|
|
666
|
+
struct Bomb {
|
|
667
|
+
panics: bool,
|
|
668
|
+
_label: String,
|
|
669
|
+
}
|
|
670
|
+
impl Drop for Bomb {
|
|
671
|
+
fn drop(&mut self) {
|
|
672
|
+
if self.panics {
|
|
673
|
+
panic!("boom");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
let mut list = LinkedList::new();
|
|
679
|
+
for i in 0..6 {
|
|
680
|
+
list.push_back(Bomb {
|
|
681
|
+
panics: i == 2,
|
|
682
|
+
_label: i.to_string(),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
list.pop_back(); // leave a node on the free list too
|
|
686
|
+
let dropped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(list)));
|
|
687
|
+
assert!(dropped.is_err());
|
|
688
|
+
// the other elements, the free list and the sentinel are freed; under
|
|
689
|
+
// Miri this also checks that nothing leaks
|
|
690
|
+
}
|
|
691
|
+
}
|
|
@@ -16,13 +16,23 @@ pub unsafe fn pyobject_hash(
|
|
|
16
16
|
) -> pyo3::PyResult<u64> {
|
|
17
17
|
let py_hash = pyo3::ffi::PyObject_Hash(arg1);
|
|
18
18
|
if crate::hashbrown::util::unlikely(py_hash == -1) {
|
|
19
|
-
//
|
|
20
|
-
return Err(
|
|
19
|
+
// PyObject_Hash never returns -1 on success.
|
|
20
|
+
return Err(take_error(py));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
Ok(mix_hash(py_hash as u64))
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/// Takes the exception set by a failed C API call.
|
|
27
|
+
///
|
|
28
|
+
/// A broken C type can report an error without setting one; like CPython, raise
|
|
29
|
+
/// `SystemError` then instead of panicking (release builds abort on panic).
|
|
30
|
+
#[cold]
|
|
31
|
+
fn take_error(py: pyo3::Python<'_>) -> pyo3::PyErr {
|
|
32
|
+
pyo3::PyErr::take(py)
|
|
33
|
+
.unwrap_or_else(|| new_py_error!(PySystemError, "error return without exception set"))
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
/// Spreads entropy of a Python hash across all 64 bits.
|
|
27
37
|
///
|
|
28
38
|
/// CPython hashes are frequently low-entropy in the high bits: `hash(n) == n` for
|
|
@@ -55,7 +65,7 @@ pub unsafe fn pyobject_equal(
|
|
|
55
65
|
let boolean = pyo3::ffi::PyObject_RichCompareBool(arg1, arg2, pyo3::ffi::Py_EQ);
|
|
56
66
|
|
|
57
67
|
if boolean < 0 {
|
|
58
|
-
Err(
|
|
68
|
+
Err(take_error(py))
|
|
59
69
|
} else {
|
|
60
70
|
Ok(boolean == 1)
|
|
61
71
|
}
|
|
@@ -54,7 +54,7 @@ macro_rules! implement_view_guard {
|
|
|
54
54
|
let cache = self.cache.get();
|
|
55
55
|
let inner = cache.0.get()?;
|
|
56
56
|
|
|
57
|
-
let policy = inner.
|
|
57
|
+
let policy = inner.policy(py);
|
|
58
58
|
|
|
59
59
|
if self.initial_gv == inner.shared().generation_version().get() {
|
|
60
60
|
return Ok(policy);
|
|
@@ -166,10 +166,11 @@ impl FIFOPolicy {
|
|
|
166
166
|
|
|
167
167
|
#[inline]
|
|
168
168
|
fn decrement_indexes(&mut self, start: usize, end: usize) {
|
|
169
|
-
const MAX_FRONT_OFFSET: usize =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
const MAX_FRONT_OFFSET: usize =
|
|
170
|
+
cfg_select! {
|
|
171
|
+
feature = "use-small-offset" => u8::MAX as usize,
|
|
172
|
+
not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
|
|
173
|
+
};
|
|
173
174
|
|
|
174
175
|
// Fast path: shifting the entire front is a single counter increment.
|
|
175
176
|
// Guard against overflow; the full-normalization path below handles that case.
|
|
@@ -256,10 +256,11 @@ impl TTLPolicy {
|
|
|
256
256
|
|
|
257
257
|
#[inline]
|
|
258
258
|
fn decrement_indexes(&mut self, start: usize, end: usize) {
|
|
259
|
-
const MAX_FRONT_OFFSET: usize =
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
const MAX_FRONT_OFFSET: usize =
|
|
260
|
+
cfg_select! {
|
|
261
|
+
feature = "use-small-offset" => u8::MAX as usize,
|
|
262
|
+
not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
|
|
263
|
+
};
|
|
263
264
|
|
|
264
265
|
// Fast path: shifting the entire front is a single counter increment.
|
|
265
266
|
// Guard against overflow; the full-normalization path below handles that case.
|
|
@@ -422,12 +423,11 @@ impl PolicyExt for TTLPolicy {
|
|
|
422
423
|
}
|
|
423
424
|
|
|
424
425
|
fn evict(&mut self, shared: &Self::Shared) -> pyo3::PyResult<Self::Handle> {
|
|
425
|
-
|
|
426
|
-
if front.is_none() {
|
|
427
|
-
return Err(new_py_error!(PyKeyError, ()));
|
|
428
|
-
}
|
|
426
|
+
self.expire(shared.generation_version());
|
|
429
427
|
|
|
430
|
-
let front =
|
|
428
|
+
let Some(front) = self.entries.front() else {
|
|
429
|
+
return Err(new_py_error!(PyKeyError, ()));
|
|
430
|
+
};
|
|
431
431
|
|
|
432
432
|
let eq = |index: &usize| Ok::<_, pyo3::PyErr>(*index - self.front_offset == 0);
|
|
433
433
|
let removed_key_table = self.table.remove_entry(front.key().hash(), eq).unwrap();
|
|
@@ -391,11 +391,12 @@ impl PolicyExt for VTTLPolicy {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
fn evict(&mut self, shared: &Self::Shared) -> pyo3::PyResult<Self::Handle> {
|
|
394
|
+
self.expire(shared.generation_version());
|
|
395
|
+
|
|
394
396
|
{
|
|
395
|
-
let front_cursor = self
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.ok_or_else(|| new_py_error!(PyKeyError, "cache is empty"))?;
|
|
397
|
+
let Some(front_cursor) = self.heap.front() else {
|
|
398
|
+
return Err(new_py_error!(PyKeyError, "cache is empty"));
|
|
399
|
+
};
|
|
399
400
|
|
|
400
401
|
self.table
|
|
401
402
|
.remove_entry(unsafe { front_cursor.element().key.hash() }, |x| {
|