The Hashable object was moved in python 3.3 and support for the old location is dropped in python 3.10 the new location is available in python 3.8 so we can just update this and it should work with both python 3.8 and 3.11 https://docs.python.org/3.8/library/collections.html
253 lines
9.3 KiB
Python
253 lines
9.3 KiB
Python
"""
|
|
Utilities related to caching.
|
|
"""
|
|
|
|
|
|
import collections
|
|
import functools
|
|
import itertools
|
|
import zlib
|
|
import pickle
|
|
|
|
import wrapt
|
|
from django.db.models.signals import post_save, post_delete
|
|
from django.utils.encoding import force_str
|
|
|
|
from edx_django_utils.cache import RequestCache, TieredCache
|
|
|
|
|
|
def request_cached(namespace=None, arg_map_function=None, request_cache_getter=None):
|
|
"""
|
|
A function decorator that automatically handles caching its return value for
|
|
the duration of the request. It returns the cached value for subsequent
|
|
calls to the same function, with the same parameters, within a given request.
|
|
|
|
Notes:
|
|
- We convert arguments and keyword arguments to their string form to build the cache key. So if you have
|
|
args/kwargs that can't be converted to strings, you're gonna have a bad time (don't do it).
|
|
- Cache key cardinality depends on the args/kwargs. So if you're caching a function that takes five arguments,
|
|
you might have deceptively low cache efficiency. Prefer functions with fewer arguments.
|
|
- WATCH OUT: Don't use this decorator for instance methods that take in a "self" argument that changes each
|
|
time the method is called. This will result in constant cache misses and not provide the performance benefit
|
|
you are looking for. Rather, change your instance method to a class method.
|
|
- Benchmark, benchmark, benchmark! If you never measure, how will you know you've improved? or regressed?
|
|
|
|
Arguments:
|
|
namespace (string): An optional namespace to use for the cache. By default, we use the default request cache,
|
|
not a namespaced request cache. Since the code automatically creates a unique cache key with the module and
|
|
function's name, storing the cached value in the default cache, you won't usually need to specify a
|
|
namespace value.
|
|
But you can specify a namespace value here if you need to use your own namespaced cache - for example,
|
|
if you want to clear out your own cache by calling RequestCache(namespace=NAMESPACE).clear().
|
|
NOTE: This argument is ignored if you supply a ``request_cache_getter``.
|
|
arg_map_function (function: arg->string): Function to use for mapping the wrapped function's arguments to
|
|
strings to use in the cache key. If not provided, defaults to force_text, which converts the given
|
|
argument to a string.
|
|
request_cache_getter (function: args, kwargs->RequestCache): Function that returns the RequestCache to use.
|
|
If not provided, defaults to edx_django_utils.cache.RequestCache. If ``request_cache_getter`` returns None,
|
|
the function's return values are not cached.
|
|
|
|
Returns:
|
|
func: a wrapper function which will call the wrapped function, passing in the same args/kwargs,
|
|
cache the value it returns, and return that cached value for subsequent calls with the
|
|
same args/kwargs within a single request.
|
|
"""
|
|
@wrapt.decorator
|
|
def decorator(wrapped, instance, args, kwargs):
|
|
"""
|
|
Arguments:
|
|
args, kwargs: values passed into the wrapped function
|
|
"""
|
|
# Check to see if we have a result in cache. If not, invoke our wrapped
|
|
# function. Cache and return the result to the caller.
|
|
if request_cache_getter:
|
|
request_cache = request_cache_getter(args if instance is None else (instance,) + args, kwargs)
|
|
else:
|
|
request_cache = RequestCache(namespace)
|
|
|
|
if request_cache:
|
|
cache_key = _func_call_cache_key(wrapped, arg_map_function, *args, **kwargs)
|
|
cached_response = request_cache.get_cached_response(cache_key)
|
|
if cached_response.is_found:
|
|
return cached_response.value
|
|
|
|
result = wrapped(*args, **kwargs)
|
|
|
|
if request_cache:
|
|
request_cache.set(cache_key, result)
|
|
|
|
return result
|
|
|
|
return decorator
|
|
|
|
|
|
def _func_call_cache_key(func, arg_map_function, *args, **kwargs):
|
|
"""
|
|
Returns a cache key based on the function's module,
|
|
the function's name, a stringified list of arguments
|
|
and a stringified list of keyword arguments.
|
|
"""
|
|
arg_map_function = arg_map_function or force_str
|
|
|
|
converted_args = list(map(arg_map_function, args))
|
|
converted_kwargs = list(map(arg_map_function, _sorted_kwargs_list(kwargs)))
|
|
|
|
cache_keys = [func.__module__, func.__name__] + converted_args + converted_kwargs
|
|
return '.'.join(cache_keys)
|
|
|
|
|
|
def _sorted_kwargs_list(kwargs):
|
|
"""
|
|
Returns a unique and deterministic ordered list from the given kwargs.
|
|
"""
|
|
sorted_kwargs = sorted(kwargs.items())
|
|
sorted_kwargs_list = list(itertools.chain(*sorted_kwargs))
|
|
return sorted_kwargs_list
|
|
|
|
|
|
class process_cached: # pylint: disable=invalid-name
|
|
"""
|
|
Decorator to cache the result of a function for the life of a process.
|
|
|
|
If the return value of the function for the provided arguments has not
|
|
yet been cached, the function will be calculated and cached. If called
|
|
later with the same arguments, the cached value is returned
|
|
(not reevaluated).
|
|
https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize
|
|
|
|
WARNING: Only use this process_cached decorator for caching data that
|
|
is constant throughout the lifetime of a gunicorn worker process,
|
|
is costly to compute, and is required often. Otherwise, it can lead to
|
|
unwanted memory leakage.
|
|
"""
|
|
|
|
def __init__(self, func):
|
|
self.func = func
|
|
self.cache = {}
|
|
|
|
def __call__(self, *args):
|
|
if not isinstance(args, collections.abc.Hashable):
|
|
# uncacheable. a list, for instance.
|
|
# better to not cache than blow up.
|
|
return self.func(*args)
|
|
if args in self.cache:
|
|
return self.cache[args]
|
|
else:
|
|
value = self.func(*args)
|
|
self.cache[args] = value
|
|
return value
|
|
|
|
def __repr__(self):
|
|
"""
|
|
Return the function's docstring.
|
|
"""
|
|
return self.func.__doc__
|
|
|
|
def __get__(self, obj, objtype):
|
|
"""
|
|
Support instance methods.
|
|
"""
|
|
partial = functools.partial(self.__call__, obj)
|
|
# Make the cache accessible on the wrapped object so it can be cleared if needed.
|
|
partial.cache = self.cache
|
|
return partial
|
|
|
|
|
|
class CacheInvalidationManager:
|
|
"""
|
|
This class provides a decorator for simple functions, which can handle invalidation.
|
|
|
|
To use, instantiate with a namespace or django model class:
|
|
`manager = CacheInvalidationManager(model=User)`
|
|
One of namespace or model should be specified, but not both.
|
|
|
|
Then use it as a decorator on functions with no arguments
|
|
`@manager
|
|
def get_system_user():
|
|
...
|
|
`
|
|
When the User model is saved or deleted, all cache keys used by
|
|
the decorator will be cleared.
|
|
"""
|
|
|
|
def __init__(self, namespace=None, model=None, cache_time=86400):
|
|
if model:
|
|
post_save.connect(self.invalidate, sender=model)
|
|
post_delete.connect(self.invalidate, sender=model)
|
|
namespace = f"{model.__module__}.{model.__qualname__}"
|
|
self.namespace = namespace
|
|
self.cache_time = cache_time
|
|
self.keys = set()
|
|
|
|
# pylint: disable=unused-argument
|
|
def invalidate(self, **kwargs):
|
|
"""
|
|
Invalidate all keys tracked by the manager.
|
|
"""
|
|
for key in self.keys:
|
|
TieredCache.delete_all_tiers(key)
|
|
|
|
def __call__(self, func):
|
|
"""
|
|
Decorator for functions with no arguments.
|
|
"""
|
|
cache_key = f'{self.namespace}.{func.__module__}.{func.__name__}'
|
|
self.keys.add(cache_key)
|
|
|
|
@functools.wraps(func)
|
|
def decorator(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring
|
|
result = TieredCache.get_cached_response(cache_key)
|
|
if result.is_found:
|
|
return result.value
|
|
result = func()
|
|
TieredCache.set_all_tiers(cache_key, result, self.cache_time)
|
|
return result
|
|
return decorator
|
|
|
|
|
|
def zpickle(data):
|
|
"""Given any data structure, returns a zlib compressed pickled serialization."""
|
|
return zlib.compress(pickle.dumps(data, 4)) # Keep this constant as we upgrade from python 2 to 3.
|
|
|
|
|
|
def zunpickle(zdata):
|
|
"""Given a zlib compressed pickled serialization, returns the deserialized data."""
|
|
return pickle.loads(zlib.decompress(zdata), encoding='latin1')
|
|
|
|
|
|
def get_cache(name):
|
|
"""
|
|
Return the request cache named ``name``.
|
|
|
|
Arguments:
|
|
name (str): The name of the request cache to load
|
|
|
|
Returns: dict
|
|
"""
|
|
assert name is not None
|
|
return RequestCache(name).data
|
|
|
|
|
|
class CacheService:
|
|
"""
|
|
An XBlock service which provides a cache.
|
|
|
|
Args:
|
|
cache(object): provides get/set functions for retrieving/storing key/value pairs.
|
|
"""
|
|
def __init__(self, cache, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._cache = cache
|
|
|
|
def get(self, key, *args, **kwargs):
|
|
"""
|
|
Returns the value cached against the given key, or None.
|
|
"""
|
|
return self._cache.get(key, *args, **kwargs)
|
|
|
|
def set(self, key, value, *args, **kwargs):
|
|
"""
|
|
Caches the value against the given key.
|
|
"""
|
|
return self._cache.set(key, value, *args, **kwargs)
|