Skip to content

Commit 9c9d4b8

Browse files
Thomas Kemmertkem
Thomas Kemmer
authored andcommitted
Fix #294: Avoid "cache stampedes" with cachetools.func decorators.
1 parent c940373 commit 9c9d4b8

File tree

3 files changed

+63
-3
lines changed

3 files changed

+63
-3
lines changed

src/cachetools/_decorators.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,50 @@
11
"""Extensible memoizing decorator helpers."""
22

33

4+
def _cached_cond_info(func, cache, key, cond, info):
5+
hits = misses = 0
6+
pending = set()
7+
8+
def wrapper(*args, **kwargs):
9+
nonlocal hits, misses
10+
k = key(*args, **kwargs)
11+
with cond:
12+
cond.wait_for(lambda: k not in pending)
13+
try:
14+
result = cache[k]
15+
hits += 1
16+
return result
17+
except KeyError:
18+
pending.add(k)
19+
misses += 1
20+
try:
21+
v = func(*args, **kwargs)
22+
with cond:
23+
try:
24+
cache[k] = v
25+
except ValueError:
26+
pass # value too large
27+
return v
28+
finally:
29+
with cond:
30+
pending.remove(k)
31+
cond.notify_all()
32+
33+
def cache_clear():
34+
nonlocal hits, misses
35+
with cond:
36+
cache.clear()
37+
hits = misses = 0
38+
39+
def cache_info():
40+
with cond:
41+
return info(hits, misses)
42+
43+
wrapper.cache_clear = cache_clear
44+
wrapper.cache_info = cache_info
45+
return wrapper
46+
47+
448
def _cached_locked_info(func, cache, key, lock, info):
549
hits = misses = 0
650

@@ -139,6 +183,8 @@ def _cached_wrapper(func, cache, key, lock, info):
139183
wrapper = _uncached_info(func, info)
140184
elif lock is None:
141185
wrapper = _cached_unlocked_info(func, cache, key, info)
186+
elif hasattr(lock, "wait_for") and hasattr(lock, "notify_all"):
187+
wrapper = _cached_cond_info(func, cache, key, lock, info)
142188
else:
143189
wrapper = _cached_locked_info(func, cache, key, lock, info)
144190
else:

src/cachetools/func.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import time
88

99
try:
10-
from threading import RLock
10+
from threading import Condition
1111
except ImportError: # pragma: no cover
12-
from dummy_threading import RLock
12+
from dummy_threading import Condition
1313

1414
from . import FIFOCache, LFUCache, LRUCache, RRCache, TTLCache
1515
from . import cached
@@ -28,7 +28,7 @@ def maxsize(self):
2828
def _cache(cache, maxsize, typed):
2929
def decorator(func):
3030
key = keys.typedkey if typed else keys.hashkey
31-
wrapper = cached(cache=cache, key=key, lock=RLock(), info=True)(func)
31+
wrapper = cached(cache=cache, key=key, lock=Condition(), info=True)(func)
3232
wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed}
3333
return wrapper
3434

tests/test_cached.py

+14
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,20 @@ def test_zero_size_cache_decorator_info(self):
214214
self.assertEqual(wrapper(0), 0)
215215
self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
216216

217+
def test_zero_size_cache_decorator_lock_info(self):
218+
cache = self.cache(0)
219+
lock = CountedLock()
220+
wrapper = cachetools.cached(cache, lock=lock, info=True)(self.func)
221+
222+
self.assertEqual(len(cache), 0)
223+
self.assertEqual(wrapper.cache_info(), (0, 0, 0, 0))
224+
self.assertEqual(lock.count, 1)
225+
self.assertEqual(wrapper(0), 0)
226+
self.assertEqual(len(cache), 0)
227+
self.assertEqual(lock.count, 3)
228+
self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
229+
self.assertEqual(lock.count, 4)
230+
217231

218232
class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
219233
def cache(self, minsize):

0 commit comments

Comments
 (0)