* Deprecates ModuleSystem can_execute_unsafe_code, get_python_lib_zip and cache properties * Adds a new CacheService and SandboxService to provide the deprecated property * Adds tests for the added CacheService and SandboxService * Updates the ModuleSystemShim tests in Lms and Studio
250 lines
9.1 KiB
Python
250 lines
9.1 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.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.
|
|
"""
|
|
return functools.partial(self.__call__, obj)
|
|
|
|
|
|
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)
|