Per-request cache in Django?

前端 未结 7 1328
余生分开走
余生分开走 2020-12-08 11:39

I would like to implement a decorator that provides per-request caching to any method, not just views. Here is an example use case.

I have a custom ta

7条回答
  •  既然无缘
    2020-12-08 12:09

    EDIT:

    The eventual solution I came up with has been compiled into a PyPI package: https://pypi.org/project/django-request-cache/

    EDIT 2016-06-15:

    I discovered a significantly simpler solution to this problem, and kindof facepalmed for not realizing how easy this should have been from the start.

    from django.core.cache.backends.base import BaseCache
    from django.core.cache.backends.locmem import LocMemCache
    from django.utils.synch import RWLock
    
    
    class RequestCache(LocMemCache):
        """
        RequestCache is a customized LocMemCache which stores its data cache as an instance attribute, rather than
        a global. It's designed to live only as long as the request object that RequestCacheMiddleware attaches it to.
        """
    
        def __init__(self):
            # We explicitly do not call super() here, because while we want BaseCache.__init__() to run, we *don't*
            # want LocMemCache.__init__() to run, because that would store our caches in its globals.
            BaseCache.__init__(self, {})
    
            self._cache = {}
            self._expire_info = {}
            self._lock = RWLock()
    
    class RequestCacheMiddleware(object):
        """
        Creates a fresh cache instance as request.cache. The cache instance lives only as long as request does.
        """
    
        def process_request(self, request):
            request.cache = RequestCache()
    

    With this, you can use request.cache as a cache instance that lives only as long as the request does, and will be fully cleaned up by the garbage collector when the request is done.

    If you need access to the request object from a context where it's not normally available, you can use one of the various implementations of a so-called "global request middleware" that can be found online.

    ** Initial answer: **

    A major problem that no other solution here solves is the fact that LocMemCache leaks memory when you create and destroy several of them over the life of a single process. django.core.cache.backends.locmem defines several global dictionaries that hold references to every LocalMemCache instance's cache data, and those dictionaries are never emptied.

    The following code solves this problem. It started as a combination of @href_'s answer and the cleaner logic used by the code linked in @squarelogic.hayden's comment, which I then refined further.

    from uuid import uuid4
    from threading import current_thread
    
    from django.core.cache.backends.base import BaseCache
    from django.core.cache.backends.locmem import LocMemCache
    from django.utils.synch import RWLock
    
    
    # Global in-memory store of cache data. Keyed by name, to provides multiple
    # named local memory caches.
    _caches = {}
    _expire_info = {}
    _locks = {}
    
    
    class RequestCache(LocMemCache):
        """
        RequestCache is a customized LocMemCache with a destructor, ensuring that creating
        and destroying RequestCache objects over and over doesn't leak memory.
        """
    
        def __init__(self):
            # We explicitly do not call super() here, because while we want
            # BaseCache.__init__() to run, we *don't* want LocMemCache.__init__() to run.
            BaseCache.__init__(self, {})
    
            # Use a name that is guaranteed to be unique for each RequestCache instance.
            # This ensures that it will always be safe to call del _caches[self.name] in
            # the destructor, even when multiple threads are doing so at the same time.
            self.name = uuid4()
            self._cache = _caches.setdefault(self.name, {})
            self._expire_info = _expire_info.setdefault(self.name, {})
            self._lock = _locks.setdefault(self.name, RWLock())
    
        def __del__(self):
            del _caches[self.name]
            del _expire_info[self.name]
            del _locks[self.name]
    
    
    class RequestCacheMiddleware(object):
        """
        Creates a cache instance that persists only for the duration of the current request.
        """
    
        _request_caches = {}
    
        def process_request(self, request):
            # The RequestCache object is keyed on the current thread because each request is
            # processed on a single thread, allowing us to retrieve the correct RequestCache
            # object in the other functions.
            self._request_caches[current_thread()] = RequestCache()
    
        def process_response(self, request, response):
            self.delete_cache()
            return response
    
        def process_exception(self, request, exception):
            self.delete_cache()
    
        @classmethod
        def get_cache(cls):
            """
            Retrieve the current request's cache.
    
            Returns None if RequestCacheMiddleware is not currently installed via 
            MIDDLEWARE_CLASSES, or if there is no active request.
            """
            return cls._request_caches.get(current_thread())
    
        @classmethod
        def clear_cache(cls):
            """
            Clear the current request's cache.
            """
            cache = cls.get_cache()
            if cache:
                cache.clear()
    
        @classmethod
        def delete_cache(cls):
            """
            Delete the current request's cache object to avoid leaking memory.
            """
            cache = cls._request_caches.pop(current_thread(), None)
            del cache
    

    EDIT 2016-06-15: I discovered a significantly simpler solution to this problem, and kindof facepalmed for not realizing how easy this should have been from the start.

    from django.core.cache.backends.base import BaseCache
    from django.core.cache.backends.locmem import LocMemCache
    from django.utils.synch import RWLock
    
    
    class RequestCache(LocMemCache):
        """
        RequestCache is a customized LocMemCache which stores its data cache as an instance attribute, rather than
        a global. It's designed to live only as long as the request object that RequestCacheMiddleware attaches it to.
        """
    
        def __init__(self):
            # We explicitly do not call super() here, because while we want BaseCache.__init__() to run, we *don't*
            # want LocMemCache.__init__() to run, because that would store our caches in its globals.
            BaseCache.__init__(self, {})
    
            self._cache = {}
            self._expire_info = {}
            self._lock = RWLock()
    
    class RequestCacheMiddleware(object):
        """
        Creates a fresh cache instance as request.cache. The cache instance lives only as long as request does.
        """
    
        def process_request(self, request):
            request.cache = RequestCache()
    

    With this, you can use request.cache as a cache instance that lives only as long as the request does, and will be fully cleaned up by the garbage collector when the request is done.

    If you need access to the request object from a context where it's not normally available, you can use one of the various implementations of a so-called "global request middleware" that can be found online.

提交回复
热议问题