Skip to content

Commit 7f8293b

Browse files
committed
Buffer(keep_slow=True)
1 parent fed29a4 commit 7f8293b

6 files changed

Lines changed: 165 additions & 90 deletions

File tree

doc/source/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Changelog
77
- New method ``File.link()``, which acquires a file-based key from another source
88
(e.g. a different memory-mapped File object)
99
(:pr:`80`) `Guido Imperiale`_
10+
- ``Buffer`` has gained the option to preserve keys in ``slow`` when they are
11+
moved back to ``fast``
12+
(:pr:`80`) `Guido Imperiale`_
1013

1114

1215
3.0.0 - 2023-04-17

zict/buffer.py

Lines changed: 38 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
from __future__ import annotations
22

33
from collections.abc import Callable, Iterator, MutableMapping
4-
from itertools import chain
5-
from typing import ( # TODO import from collections.abc (needs Python >=3.9)
6-
ItemsView,
7-
ValuesView,
8-
)
94

105
from zict.common import KT, VT, ZictBase, close, discard, flush, locked
116
from zict.lru import LRU
7+
from zict.utils import InsertionSortedSet
128

139

1410
class Buffer(ZictBase[KT, VT]):
@@ -34,6 +30,11 @@ class Buffer(ZictBase[KT, VT]):
3430
storing to disk and raised a disk full error) the key will remain in the LRU.
3531
slow_to_fast_callbacks: list of callables
3632
These functions run every time data moves form the slow to the fast mapping.
33+
keep_slow: bool, optional
34+
If False (default), delete key/value pairs in slow when they are moved back to
35+
fast.
36+
If True, keep them in slow until deleted; this will avoid repeating the fast to
37+
slow transition when they are evicted again, but at the cost of duplication.
3738
3839
Notes
3940
-----
@@ -60,7 +61,9 @@ class Buffer(ZictBase[KT, VT]):
6061
weight: Callable[[KT, VT], float]
6162
fast_to_slow_callbacks: list[Callable[[KT, VT], None]]
6263
slow_to_fast_callbacks: list[Callable[[KT, VT], None]]
64+
keep_slow: bool
6365
_cancel_restore: dict[KT, bool]
66+
_keys: InsertionSortedSet[KT]
6467

6568
def __init__(
6669
self,
@@ -74,6 +77,7 @@ def __init__(
7477
slow_to_fast_callbacks: Callable[[KT, VT], None]
7578
| list[Callable[[KT, VT], None]]
7679
| None = None,
80+
keep_slow: bool = False,
7781
):
7882
super().__init__()
7983
self.fast = LRU(
@@ -91,7 +95,9 @@ def __init__(
9195
slow_to_fast_callbacks = [slow_to_fast_callbacks]
9296
self.fast_to_slow_callbacks = fast_to_slow_callbacks or []
9397
self.slow_to_fast_callbacks = slow_to_fast_callbacks or []
98+
self.keep_slow = keep_slow
9499
self._cancel_restore = {}
100+
self._keys = InsertionSortedSet((*self.fast, *self.slow))
95101

96102
@property
97103
def n(self) -> float:
@@ -136,6 +142,9 @@ def offset(self, value: float) -> None:
136142
self.fast.offset = value
137143

138144
def fast_to_slow(self, key: KT, value: VT) -> None:
145+
if self.keep_slow and key in self.slow:
146+
return
147+
139148
self.slow[key] = value
140149
try:
141150
for cb in self.fast_to_slow_callbacks:
@@ -169,7 +178,8 @@ def slow_to_fast(self, key: KT) -> VT:
169178
# - If the below code was just `self.fast[key] = value; del
170179
# self.slow[key]` now the key would be in neither slow nor fast!
171180
self.fast.set_noevict(key, value)
172-
del self.slow[key]
181+
if not self.keep_slow:
182+
del self.slow[key]
173183

174184
with self.unlock():
175185
self.fast.evict_until_below_target()
@@ -180,27 +190,31 @@ def slow_to_fast(self, key: KT) -> VT:
180190

181191
@locked
182192
def __getitem__(self, key: KT) -> VT:
193+
if key not in self._keys:
194+
raise KeyError(key)
183195
try:
184196
return self.fast[key]
185197
except KeyError:
186198
return self.slow_to_fast(key)
187199

188200
def __setitem__(self, key: KT, value: VT) -> None:
189-
with self.lock:
190-
discard(self.slow, key)
191-
if key in self._cancel_restore:
192-
self._cancel_restore[key] = True
193-
self.fast[key] = value
201+
self.set_noevict(key, value)
202+
try:
203+
self.fast.evict_until_below_target()
204+
except Exception:
205+
self.fast._setitem_exception(key)
206+
raise
194207

195208
@locked
196209
def set_noevict(self, key: KT, value: VT) -> None:
197210
"""Variant of ``__setitem__`` that does not move keys from fast to slow if the
198211
total weight exceeds n
199212
"""
200-
discard(self.slow, key)
213+
discard(self, key)
201214
if key in self._cancel_restore:
202215
self._cancel_restore[key] = True
203216
self.fast.set_noevict(key, value)
217+
self._keys.add(key)
204218

205219
def evict_until_below_target(self, n: float | None = None) -> None:
206220
"""Wrapper around :meth:`zict.LRU.evict_until_below_target`.
@@ -210,55 +224,32 @@ def evict_until_below_target(self, n: float | None = None) -> None:
210224

211225
@locked
212226
def __delitem__(self, key: KT) -> None:
227+
self._keys.remove(key)
213228
if key in self._cancel_restore:
214229
self._cancel_restore[key] = True
215-
try:
216-
del self.fast[key]
217-
except KeyError:
218-
del self.slow[key]
230+
discard(self.fast, key)
231+
discard(self.slow, key)
219232

220233
@locked
221234
def _cancel_evict(self, key: KT, value: VT) -> None:
222235
discard(self.slow, key)
223236

224-
def values(self) -> ValuesView[VT]:
225-
return BufferValuesView(self)
226-
227-
def items(self) -> ItemsView[KT, VT]:
228-
return BufferItemsView(self)
229-
230237
def __len__(self) -> int:
231-
with self.lock, self.fast.lock:
232-
return (
233-
len(self.fast)
234-
+ len(self.slow)
235-
- sum(
236-
k in self.fast and k in self.slow
237-
for k in chain(self._cancel_restore, self.fast._cancel_evict)
238-
)
239-
)
238+
return len(self._keys)
240239

241240
def __iter__(self) -> Iterator[KT]:
242-
"""Make sure that the iteration is not disrupted if you evict/restore a key in
243-
the middle of it
244-
"""
245-
seen = set()
246-
while True:
247-
try:
248-
for d in (self.fast, self.slow):
249-
for key in d:
250-
if key not in seen:
251-
seen.add(key)
252-
yield key
253-
return
254-
except RuntimeError:
255-
pass
241+
return iter(self._keys)
256242

257243
def __contains__(self, key: object) -> bool:
258-
return key in self.fast or key in self.slow
244+
return key in self._keys
259245

246+
@locked
260247
def __str__(self) -> str:
261-
return f"Buffer<{self.fast}, {self.slow}>"
248+
s = f"Buffer<fast: {len(self.fast)}, slow: {len(self.slow)}"
249+
if self.keep_slow:
250+
ndup = len(self.fast) + len(self.slow) - len(self._keys)
251+
s += f", unique: {len(self._keys)}, duplicates: {ndup}"
252+
return s + ">"
262253

263254
__repr__ = __str__
264255

@@ -267,25 +258,3 @@ def flush(self) -> None:
267258

268259
def close(self) -> None:
269260
close(self.fast, self.slow)
270-
271-
272-
class BufferItemsView(ItemsView[KT, VT]):
273-
_mapping: Buffer # FIXME CPython implementation detail
274-
__slots__ = ()
275-
276-
def __iter__(self) -> Iterator[tuple[KT, VT]]:
277-
# Avoid changing the LRU
278-
return chain(self._mapping.fast.items(), self._mapping.slow.items())
279-
280-
281-
class BufferValuesView(ValuesView[VT]):
282-
_mapping: Buffer # FIXME CPython implementation detail
283-
__slots__ = ()
284-
285-
def __contains__(self, value: object) -> bool:
286-
# Avoid changing the LRU
287-
return any(value == v for v in self)
288-
289-
def __iter__(self) -> Iterator[VT]:
290-
# Avoid changing the LRU
291-
return chain(self._mapping.fast.values(), self._mapping.slow.values())

zict/lru.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,20 @@ def __setitem__(self, key: KT, value: VT) -> None:
137137
try:
138138
self.evict_until_below_target()
139139
except Exception:
140-
if self.weights.get(key, 0) > self.n and key not in self.heavy:
141-
# weight(value) > n and evicting the key we just inserted failed.
142-
# Evict the rest of the LRU instead.
143-
try:
144-
while len(self.d) > 1:
145-
self.evict()
146-
except Exception:
147-
pass
140+
self._setitem_exception(key)
148141
raise
149142

143+
@locked
144+
def _setitem_exception(self, key: KT) -> None:
145+
if self.weights.get(key, 0) > self.n and key not in self.heavy:
146+
# weight(value) > n and evicting the key we just inserted failed.
147+
# Evict the rest of the LRU instead.
148+
try:
149+
while len(self.d) > 1:
150+
self.evict()
151+
except Exception:
152+
pass
153+
150154
@locked
151155
def set_noevict(self, key: KT, value: VT) -> None:
152156
"""Variant of ``__setitem__`` that does not evict if the total weight exceeds n.

zict/tests/test_async_buffer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ async def test_close_during_evict(check_thread_leaks):
132132

133133
@pytest.mark.asyncio
134134
async def test_close_during_get(check_thread_leaks):
135-
buff = AsyncBuffer({}, utils_test.SlowDict(0.01), n=100)
136-
buff.slow.data.update({i: i for i in range(100)})
135+
buff = AsyncBuffer({}, utils_test.SlowDict(0.01, {i: i for i in range(100)}), n=100)
137136
assert len(buff) == 100
138137
assert not buff.fast
139138

@@ -199,8 +198,7 @@ def __getitem__(self, key):
199198
time.sleep(0.01)
200199
return super().__getitem__(key)
201200

202-
with AsyncBuffer({}, Slow(), n=100) as buff:
203-
buff.slow.update({i: i for i in range(100)})
201+
with AsyncBuffer({}, Slow({i: i for i in range(100)}), n=100) as buff:
204202
assert len(buff) == 100
205203

206204
future = buff.async_get(list(range(100)), missing=missing)

0 commit comments

Comments
 (0)