Skip to content

Commit b2b023c

Browse files
miss-islingtonrhettinger
authored andcommitted
bpo-35780: Fix errors in lru_cache() C code (GH-11623) (GH-11682)
1 parent a6a8524 commit b2b023c

File tree

4 files changed

+234
-89
lines changed

4 files changed

+234
-89
lines changed

Lib/functools.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def __hash__(self):
413413

414414
def _make_key(args, kwds, typed,
415415
kwd_mark = (object(),),
416-
fasttypes = {int, str, frozenset, type(None)},
416+
fasttypes = {int, str},
417417
tuple=tuple, type=type, len=len):
418418
"""Make a cache key from optionally typed positional and keyword arguments
419419
@@ -469,8 +469,11 @@ def lru_cache(maxsize=128, typed=False):
469469

470470
# Early detection of an erroneous call to @lru_cache without any arguments
471471
# resulting in the inner function being passed to maxsize instead of an
472-
# integer or None.
473-
if maxsize is not None and not isinstance(maxsize, int):
472+
# integer or None. Negative maxsize is treated as 0.
473+
if isinstance(maxsize, int):
474+
if maxsize < 0:
475+
maxsize = 0
476+
elif maxsize is not None:
474477
raise TypeError('Expected maxsize to be an integer or None')
475478

476479
def decorating_function(user_function):
@@ -537,6 +540,7 @@ def wrapper(*args, **kwds):
537540
link[NEXT] = root
538541
hits += 1
539542
return result
543+
misses += 1
540544
result = user_function(*args, **kwds)
541545
with lock:
542546
if key in cache:
@@ -574,7 +578,6 @@ def wrapper(*args, **kwds):
574578
# Use the cache_len bound method instead of the len() function
575579
# which could potentially be wrapped in an lru_cache itself.
576580
full = (cache_len() >= maxsize)
577-
misses += 1
578581
return result
579582

580583
def cache_info():

Lib/test/test_functools.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,33 @@ def f(x):
12261226
self.assertEqual(misses, 4)
12271227
self.assertEqual(currsize, 2)
12281228

1229+
def test_lru_bug_35780(self):
1230+
# C version of the lru_cache was not checking to see if
1231+
# the user function call has already modified the cache
1232+
# (this arises in recursive calls and in multi-threading).
1233+
# This cause the cache to have orphan links not referenced
1234+
# by the cache dictionary.
1235+
1236+
once = True # Modified by f(x) below
1237+
1238+
@self.module.lru_cache(maxsize=10)
1239+
def f(x):
1240+
nonlocal once
1241+
rv = f'.{x}.'
1242+
if x == 20 and once:
1243+
once = False
1244+
rv = f(x)
1245+
return rv
1246+
1247+
# Fill the cache
1248+
for x in range(15):
1249+
self.assertEqual(f(x), f'.{x}.')
1250+
self.assertEqual(f.cache_info().currsize, 10)
1251+
1252+
# Make a recursive call and make sure the cache remains full
1253+
self.assertEqual(f(20), '.20.')
1254+
self.assertEqual(f.cache_info().currsize, 10)
1255+
12291256
def test_lru_hash_only_once(self):
12301257
# To protect against weird reentrancy bugs and to improve
12311258
# efficiency when faced with slow __hash__ methods, the
@@ -1322,7 +1349,7 @@ def eq(n):
13221349
for i in (0, 1):
13231350
self.assertEqual([eq(n) for n in range(150)], list(range(150)))
13241351
self.assertEqual(eq.cache_info(),
1325-
self.module._CacheInfo(hits=0, misses=300, maxsize=-10, currsize=1))
1352+
self.module._CacheInfo(hits=0, misses=300, maxsize=0, currsize=0))
13261353

13271354
def test_lru_with_exceptions(self):
13281355
# Verify that user_function exceptions get passed through without
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Fix lru_cache() errors arising in recursive, reentrant, or
2+
multi-threaded code. These errors could result in orphan links and in
3+
the cache being trapped in a state with fewer than the specified maximum
4+
number of links. Fix handling of negative maxsize which should have
5+
been treated as zero. Fix errors in toggling the "full" status flag.
6+
Fix misordering of links when errors are encountered. Sync-up the C
7+
code and pure Python code for the space saving path in functions with a
8+
single positional argument. In this common case, the space overhead of
9+
an lru cache entry is reduced by almost half. Fix counting of cache
10+
misses. In error cases, the miss count was out of sync with the actual
11+
number of times the underlying user function was called.

0 commit comments

Comments
 (0)