Skip to content

Commit a50cb38

Browse files
authored
Merge pull request #25 from ycharts/feature/cache-key-consistency-improvements
Improve consistency of cache key generation with args/kwargs
2 parents 3da2eb9 + b9f26e3 commit a50cb38

File tree

2 files changed

+203
-33
lines changed

2 files changed

+203
-33
lines changed

cache_helper/decorators.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
from inspect import Signature, signature
3+
from typing import Tuple # deprecated, but required for Python 3.8 and below
24

35
try:
46
from _pylibmc import Error as CacheSetError
@@ -15,23 +17,41 @@
1517
logger = logging.getLogger(__name__)
1618

1719

20+
def _get_function_cache_keys(func_name: str, func_signature: Signature, args: tuple, kwargs: dict) -> Tuple[str, str]:
21+
"""
22+
Generate hashed and non-hashed function cache keys, ensuring that args and kwargs are correctly bound to function.
23+
24+
:param func_name: The fully specified name of the function to be cached.
25+
:param func_signature: The signature of the function to be cached.
26+
:param args: The positional arguments passed to the function.
27+
:param kwargs: The keyword arguments passed to the function.
28+
29+
:return: A tuple containing the hashed cache key and the non-hashed cache key.
30+
"""
31+
bound_arguments = func_signature.bind(*args, **kwargs)
32+
bound_arguments.apply_defaults()
33+
cache_key_string = utils.get_function_cache_key(func_name, bound_arguments.args, bound_arguments.kwargs)
34+
cache_key_hashed = utils.get_hashed_cache_key(cache_key_string)
35+
return cache_key_hashed, cache_key_string
36+
37+
1838
def cached(timeout):
1939
def _cached(func):
2040
func_name = utils.get_function_name(func)
41+
func_signature = signature(func)
2142

2243
@wraps(func)
2344
def wrapper(*args, **kwargs):
24-
function_cache_key = utils.get_function_cache_key(func_name, args, kwargs)
25-
cache_key = utils.get_hashed_cache_key(function_cache_key)
45+
cache_key_hashed, cache_key_string = _get_function_cache_keys(func_name, func_signature, args, kwargs)
2646

2747
# We need to determine whether the object exists in the cache, and since we may have stored a literal value
2848
# None, use a sentinel object as the default
2949
sentinel = object()
3050
try:
31-
value = cache.get(cache_key, sentinel)
51+
value = cache.get(cache_key_hashed, sentinel)
3252
except Exception:
3353
logger.warning(
34-
f'Error retrieving value from Cache for Key: {function_cache_key}',
54+
f'Error retrieving value from Cache for Key: {cache_key_string}',
3555
exc_info=True,
3656
)
3757
value = sentinel
@@ -41,7 +61,7 @@ def wrapper(*args, **kwargs):
4161
if value is None:
4262
logger.warning(
4363
'None cache value found for cache key: {}, function cache key: {}, value: {}'.format(
44-
cache_key, function_cache_key, value
64+
cache_key_hashed, cache_key_string, value
4565
)
4666
)
4767

@@ -51,10 +71,10 @@ def wrapper(*args, **kwargs):
5171
# But if it fails on an error from the underlying
5272
# cache system, handle it.
5373
try:
54-
cache.set(cache_key, value, timeout)
74+
cache.set(cache_key_hashed, value, timeout)
5575
except CacheSetError:
5676
logger.warning(
57-
f'Error saving value to Cache for Key: {function_cache_key}',
77+
f'Error saving value to Cache for Key: {cache_key_string}',
5878
exc_info=True,
5979
)
6080

@@ -68,8 +88,7 @@ def invalidate(*args, **kwargs):
6888
:param kwargs: The kwargs passed into the original function.
6989
:rtype: None
7090
"""
71-
function_cache_key = utils.get_function_cache_key(func_name, args, kwargs)
72-
cache_key = utils.get_hashed_cache_key(function_cache_key)
91+
cache_key, _ = _get_function_cache_keys(func_name, func_signature, args, kwargs)
7392
cache.delete(cache_key)
7493

7594
wrapper.invalidate = invalidate
@@ -81,21 +100,23 @@ def invalidate(*args, **kwargs):
81100
def cached_class_method(timeout):
82101
def _cached(func):
83102
func_name = utils.get_function_name(func)
103+
func_signature = signature(func)
84104

85105
@wraps(func)
86106
def wrapper(*args, **kwargs):
87-
# skip the first arg because it will be the class itself
88-
function_cache_key = utils.get_function_cache_key(func_name, args[1:], kwargs)
89-
cache_key = utils.get_hashed_cache_key(function_cache_key)
90-
107+
# replace the first arg for caching purposes because it will be the class itself
108+
cls_adjusted_args = (None, *args[1:])
109+
cache_key_hashed, cache_key_string = _get_function_cache_keys(
110+
func_name, func_signature, cls_adjusted_args, kwargs
111+
)
91112
# We need to determine whether the object exists in the cache, and since we may have stored a literal value
92113
# None, use a sentinel object as the default
93114
sentinel = object()
94115
try:
95-
value = cache.get(cache_key, sentinel)
116+
value = cache.get(cache_key_hashed, sentinel)
96117
except Exception:
97118
logger.warning(
98-
f'Error retrieving value from Cache for Key: {function_cache_key}',
119+
f'Error retrieving value from Cache for Key: {cache_key_string}',
99120
exc_info=True,
100121
)
101122
value = sentinel
@@ -105,7 +126,7 @@ def wrapper(*args, **kwargs):
105126
if value is None:
106127
logger.warning(
107128
'None cache value found for cache key: {}, function cache key: {}, value: {}'.format(
108-
cache_key, function_cache_key, value
129+
cache_key_hashed, cache_key_string, value
109130
)
110131
)
111132

@@ -115,10 +136,10 @@ def wrapper(*args, **kwargs):
115136
# But if it fails on an error from the underlying
116137
# cache system, handle it.
117138
try:
118-
cache.set(cache_key, value, timeout)
139+
cache.set(cache_key_hashed, value, timeout)
119140
except CacheSetError:
120141
logger.warning(
121-
f'Error saving value to Cache for Key: {function_cache_key}',
142+
f'Error saving value to Cache for Key: {cache_key_string}',
122143
exc_info=True,
123144
)
124145

@@ -127,13 +148,16 @@ def wrapper(*args, **kwargs):
127148
def invalidate(*args, **kwargs):
128149
"""
129150
A method to invalidate a result from the cache.
130-
:param args: The args passed into the original function. This includes `self` for instance methods, and
151+
:param args: The args passed into the original function. This excludes `self` for instance methods, and
131152
`cls` for class methods.
132153
:param kwargs: The kwargs passed into the original function.
133154
:rtype: None
134155
"""
135-
function_cache_key = utils.get_function_cache_key(func_name, args, kwargs)
136-
cache_key = utils.get_hashed_cache_key(function_cache_key)
156+
# note: args does not include the class itself, but because it *is* passed to wrapper() and we replaced
157+
# it with None for consistent cache behavior with subclasses, we need to account for it here by updating
158+
# args to include None
159+
cls_adjusted_args = (None, *args)
160+
cache_key, _ = _get_function_cache_keys(func_name, func_signature, cls_adjusted_args, kwargs)
137161
cache.delete(cache_key)
138162

139163
wrapper.invalidate = invalidate
@@ -171,16 +195,16 @@ def __get__(self, obj, objtype):
171195
return fn
172196

173197
def __call__(self, *args, **kwargs):
174-
cache_key, function_cache_key = self.create_cache_key(*args, **kwargs)
198+
cache_key_hashed, cache_key_string = self.create_cache_key(*args, **kwargs)
175199

176200
# We need to determine whether the object exists in the cache, and since we may have stored a literal value
177201
# None, use a sentinel object as the default
178202
sentinel = object()
179203
try:
180-
value = cache.get(cache_key, sentinel)
204+
value = cache.get(cache_key_hashed, sentinel)
181205
except Exception:
182206
logger.warning(
183-
f'Error retrieving value from Cache for Key: {function_cache_key}',
207+
f'Error retrieving value from Cache for Key: {cache_key_string}',
184208
exc_info=True,
185209
)
186210
value = sentinel
@@ -190,7 +214,7 @@ def __call__(self, *args, **kwargs):
190214
if value is None:
191215
logger.warning(
192216
'None cache value found for cache key: {}, function cache key: {}, value: {}'.format(
193-
cache_key, function_cache_key, value
217+
cache_key_hashed, cache_key_string, value
194218
)
195219
)
196220

@@ -200,10 +224,10 @@ def __call__(self, *args, **kwargs):
200224
# But if it fails on an error from the underlying
201225
# cache system, handle it.
202226
try:
203-
cache.set(cache_key, value, timeout)
227+
cache.set(cache_key_hashed, value, timeout)
204228
except CacheSetError:
205229
logger.warning(
206-
f'Error saving value to Cache for Key: {function_cache_key}',
230+
f'Error saving value to Cache for Key: {cache_key_string}',
207231
exc_info=True,
208232
)
209233

@@ -217,14 +241,14 @@ def _invalidate(self, *args, **kwargs):
217241
:param kwargs: The kwargs passed into the original function.
218242
:rtype: None
219243
"""
220-
cache_key, _ = self.create_cache_key(*args, **kwargs)
221-
cache.delete(cache_key)
244+
cache_key_hashed, _ = self.create_cache_key(*args, **kwargs)
245+
cache.delete(cache_key_hashed)
222246

223247
def create_cache_key(self, *args, **kwargs):
224248
# Need to include the first arg (self) in the cache key
225249
func_name = utils.get_function_name(self.func)
226-
function_cache_key = utils.get_function_cache_key(func_name, args, kwargs)
227-
cache_key = utils.get_hashed_cache_key(function_cache_key)
228-
return cache_key, function_cache_key
250+
func_signature = signature(self.func)
251+
cache_key_hashed, cache_key_string = _get_function_cache_keys(func_name, func_signature, args, kwargs)
252+
return cache_key_hashed, cache_key_string
229253

230254
return wrapper

0 commit comments

Comments
 (0)