Fixed #21012 -- Thread-local caches, like databases.

This commit is contained in:
Curtis Maloney 2013-10-19 09:49:24 +11:00 committed by Aymeric Augustin
parent 3ca0815c0b
commit ee7eb0f73e
14 changed files with 303 additions and 143 deletions

View File

@ -1,6 +1,6 @@
from django.conf import settings from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase, CreateError from django.contrib.sessions.backends.base import SessionBase, CreateError
from django.core.cache import get_cache from django.core.cache import caches
from django.utils.six.moves import xrange from django.utils.six.moves import xrange
KEY_PREFIX = "django.contrib.sessions.cache" KEY_PREFIX = "django.contrib.sessions.cache"
@ -11,7 +11,7 @@ class SessionStore(SessionBase):
A cache-based session store. A cache-based session store.
""" """
def __init__(self, session_key=None): def __init__(self, session_key=None):
self._cache = get_cache(settings.SESSION_CACHE_ALIAS) self._cache = caches[settings.SESSION_CACHE_ALIAS]
super(SessionStore, self).__init__(session_key) super(SessionStore, self).__init__(session_key)
@property @property

View File

@ -6,7 +6,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.contrib.sessions.backends.db import SessionStore as DBStore from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.core.cache import get_cache from django.core.cache import caches
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -20,7 +20,7 @@ class SessionStore(DBStore):
""" """
def __init__(self, session_key=None): def __init__(self, session_key=None):
self._cache = get_cache(settings.SESSION_CACHE_ALIAS) self._cache = caches[settings.SESSION_CACHE_ALIAS]
super(SessionStore, self).__init__(session_key) super(SessionStore, self).__init__(session_key)
@property @property

View File

@ -15,7 +15,7 @@ from django.contrib.sessions.backends.file import SessionStore as FileSession
from django.contrib.sessions.backends.signed_cookies import SessionStore as CookieSession from django.contrib.sessions.backends.signed_cookies import SessionStore as CookieSession
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import get_cache from django.core.cache import caches
from django.core.cache.backends.base import InvalidCacheBackendError from django.core.cache.backends.base import InvalidCacheBackendError
from django.core import management from django.core import management
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -140,7 +140,7 @@ class SessionTestsMixin(object):
self.assertTrue(self.session.modified) self.assertTrue(self.session.modified)
def test_save(self): def test_save(self):
if (hasattr(self.session, '_cache') and'DummyCache' in if (hasattr(self.session, '_cache') and 'DummyCache' in
settings.CACHES[settings.SESSION_CACHE_ALIAS]['BACKEND']): settings.CACHES[settings.SESSION_CACHE_ALIAS]['BACKEND']):
raise unittest.SkipTest("Session saving tests require a real cache backend") raise unittest.SkipTest("Session saving tests require a real cache backend")
self.session.save() self.session.save()
@ -481,7 +481,7 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
def test_default_cache(self): def test_default_cache(self):
self.session.save() self.session.save()
self.assertNotEqual(get_cache('default').get(self.session.cache_key), None) self.assertNotEqual(caches['default'].get(self.session.cache_key), None)
@override_settings(CACHES={ @override_settings(CACHES={
'default': { 'default': {
@ -489,6 +489,7 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
}, },
'sessions': { 'sessions': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'session',
}, },
}, SESSION_CACHE_ALIAS='sessions') }, SESSION_CACHE_ALIAS='sessions')
def test_non_default_cache(self): def test_non_default_cache(self):
@ -496,8 +497,8 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
self.session = self.backend() self.session = self.backend()
self.session.save() self.session.save()
self.assertEqual(get_cache('default').get(self.session.cache_key), None) self.assertEqual(caches['default'].get(self.session.cache_key), None)
self.assertNotEqual(get_cache('sessions').get(self.session.cache_key), None) self.assertNotEqual(caches['sessions'].get(self.session.cache_key), None)
class SessionMiddlewareTests(unittest.TestCase): class SessionMiddlewareTests(unittest.TestCase):

View File

@ -7,7 +7,7 @@ import posixpath
import re import re
from django.conf import settings from django.conf import settings
from django.core.cache import (get_cache, InvalidCacheBackendError, from django.core.cache import (caches, InvalidCacheBackendError,
cache as default_cache) cache as default_cache)
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -56,7 +56,7 @@ class CachedFilesMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CachedFilesMixin, self).__init__(*args, **kwargs) super(CachedFilesMixin, self).__init__(*args, **kwargs)
try: try:
self.cache = get_cache('staticfiles') self.cache = caches['staticfiles']
except InvalidCacheBackendError: except InvalidCacheBackendError:
# Use the default backend # Use the default backend
self.cache = default_cache self.cache = default_cache

View File

@ -14,6 +14,9 @@ class.
See docs/topics/cache.txt for information on the public API. See docs/topics/cache.txt for information on the public API.
""" """
from threading import local
import warnings
from django.conf import settings from django.conf import settings
from django.core import signals from django.core import signals
from django.core.cache.backends.base import ( from django.core.cache.backends.base import (
@ -23,8 +26,8 @@ from django.utils.module_loading import import_by_path
__all__ = [ __all__ = [
'get_cache', 'cache', 'DEFAULT_CACHE_ALIAS', 'InvalidCacheBackendError', 'create_cache', 'get_cache', 'cache', 'DEFAULT_CACHE_ALIAS',
'CacheKeyWarning', 'BaseCache', 'InvalidCacheBackendError', 'CacheKeyWarning', 'BaseCache',
] ]
DEFAULT_CACHE_ALIAS = 'default' DEFAULT_CACHE_ALIAS = 'default'
@ -35,43 +38,61 @@ if DEFAULT_CACHE_ALIAS not in settings.CACHES:
def get_cache(backend, **kwargs): def get_cache(backend, **kwargs):
""" """
Function to load a cache backend dynamically. This is flexible by design Function to retrieve a configure cache, or create a new one.
to allow different use cases:
To load a backend that is pre-defined in the settings:: This wrapper is for backward compatibility.
cache = get_cache('default') Use either create_cache or caches directly.
To load a backend with its dotted import path,
including arbitrary options::
cache = get_cache('django.core.cache.backends.memcached.MemcachedCache', **{
'LOCATION': '127.0.0.1:11211', 'TIMEOUT': 30,
})
""" """
warnings.warn("'get_cache' is deprecated. Use either caches or create_cache.",
PendingDeprecationWarning, stacklevel=2)
# If it's just an alias with no options, use the new API
if backend in settings.CACHES and not kwargs:
return caches[backend]
return create_cache(backend, **kwargs)
def create_cache(backend, **params):
"""
Function to create a cache backend dynamically. This is flexible by design
to allow different use cases:
To load a backend with its dotted import path, including options::
cache = get_cache('django.core.cache.backends.memcached.MemcachedCache',
LOCATION='127.0.0.1:11211', TIMEOUT=30,
})
To create a new instance of a cache in settings.CACHES, pass the alias::
cache = create_cache('default')
You can also pass extra parameters to override those in settings.CACHES::
cache = create_cache('default', LOCATION='bar')
"""
# We can name a cache from settings.CACHES and update its params
try:
conf = settings.CACHES[backend]
except KeyError:
pass
else:
params = conf.copy()
params.update(params)
backend = params.pop('BACKEND')
try: try:
# Try to get the CACHES entry for the given backend name first
try:
conf = settings.CACHES[backend]
except KeyError:
try:
# Trying to import the given backend, in case it's a dotted path
import_by_path(backend)
except ImproperlyConfigured as e:
raise InvalidCacheBackendError("Could not find backend '%s': %s" % (
backend, e))
location = kwargs.pop('LOCATION', '')
params = kwargs
else:
params = conf.copy()
params.update(kwargs)
backend = params.pop('BACKEND')
location = params.pop('LOCATION', '')
backend_cls = import_by_path(backend) backend_cls = import_by_path(backend)
except (AttributeError, ImportError, ImproperlyConfigured) as e: except ImproperlyConfigured as e:
raise InvalidCacheBackendError( raise InvalidCacheBackendError("Could not find backend '%s': %s" % (
"Could not find backend '%s': %s" % (backend, e)) backend, e
))
location = params.pop('LOCATION', '')
cache = backend_cls(location, params) cache = backend_cls(location, params)
# Some caches -- python-memcached in particular -- need to do a cleanup at the # Some caches -- python-memcached in particular -- need to do a cleanup at the
# end of a request cycle. If not implemented in a particular backend # end of a request cycle. If not implemented in a particular backend
@ -79,4 +100,54 @@ def get_cache(backend, **kwargs):
signals.request_finished.connect(cache.close) signals.request_finished.connect(cache.close)
return cache return cache
cache = get_cache(DEFAULT_CACHE_ALIAS)
class CacheHandler(object):
"""
A Cache Handler to manage access to Cache instances.
Ensures only one instance of each alias exists per thread.
"""
def __init__(self):
self._caches = local()
def __getitem__(self, alias):
try:
return getattr(self._caches, alias)
except AttributeError:
pass
if alias not in settings.CACHES:
raise InvalidCacheBackendError(
"Could not find config for '%s' in settings.CACHES" % alias
)
cache = create_cache(alias)
setattr(self._caches, alias, cache)
return cache
caches = CacheHandler()
class DefaultCacheProxy(object):
"""
Proxy access to the default Cache object's attributes.
This allows the legacy `cache` object to be thread-safe using the new
``caches`` API.
"""
def __getattr__(self, name):
return getattr(caches[DEFAULT_CACHE_ALIAS], name)
def __setattr__(self, name, value):
return setattr(caches[DEFAULT_CACHE_ALIAS], name, value)
def __delattr__(self, name):
return delattr(caches[DEFAULT_CACHE_ALIAS], name)
def __eq__(self, other):
return caches[DEFAULT_CACHE_ALIAS] == other
def __ne__(self, other):
return caches[DEFAULT_CACHE_ALIAS] != other
cache = DefaultCacheProxy()

View File

@ -2,13 +2,13 @@
import time import time
import pickle import pickle
from threading import local
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
from django.utils import six from django.utils import six
from django.utils.deprecation import RenameMethodsBase from django.utils.deprecation import RenameMethodsBase
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.functional import cached_property
class BaseMemcachedCacheMethods(RenameMethodsBase): class BaseMemcachedCacheMethods(RenameMethodsBase):
@ -177,24 +177,14 @@ class PyLibMCCache(BaseMemcachedCache):
"An implementation of a cache binding using pylibmc" "An implementation of a cache binding using pylibmc"
def __init__(self, server, params): def __init__(self, server, params):
import pylibmc import pylibmc
self._local = local()
super(PyLibMCCache, self).__init__(server, params, super(PyLibMCCache, self).__init__(server, params,
library=pylibmc, library=pylibmc,
value_not_found_exception=pylibmc.NotFound) value_not_found_exception=pylibmc.NotFound)
@property @cached_property
def _cache(self): def _cache(self):
# PylibMC uses cache options as the 'behaviors' attribute.
# It also needs to use threadlocals, because some versions of
# PylibMC don't play well with the GIL.
client = getattr(self._local, 'client', None)
if client:
return client
client = self._lib.Client(self._servers) client = self._lib.Client(self._servers)
if self._options: if self._options:
client.behaviors = self._options client.behaviors = self._options
self._local.client = client
return client return client

View File

@ -1,7 +1,7 @@
from optparse import make_option from optparse import make_option
from django.conf import settings from django.conf import settings
from django.core.cache import get_cache from django.core.cache import create_cache
from django.core.cache.backends.db import BaseDatabaseCache from django.core.cache.backends.db import BaseDatabaseCache
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
@ -30,7 +30,7 @@ class Command(BaseCommand):
self.create_table(db, tablename) self.create_table(db, tablename)
else: else:
for cache_alias in settings.CACHES: for cache_alias in settings.CACHES:
cache = get_cache(cache_alias) cache = create_cache(cache_alias)
if isinstance(cache, BaseDatabaseCache): if isinstance(cache, BaseDatabaseCache):
self.create_table(db, cache._table) self.create_table(db, cache._table)

View File

@ -46,7 +46,7 @@ More details about how the caching works:
import warnings import warnings
from django.conf import settings from django.conf import settings
from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS from django.core.cache import create_cache, caches, DEFAULT_CACHE_ALIAS
from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age
@ -64,7 +64,7 @@ class UpdateCacheMiddleware(object):
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False)
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS
self.cache = get_cache(self.cache_alias) self.cache = caches[self.cache_alias]
def _session_accessed(self, request): def _session_accessed(self, request):
try: try:
@ -122,10 +122,9 @@ class FetchFromCacheMiddleware(object):
MIDDLEWARE_CLASSES so that it'll get called last during the request phase. MIDDLEWARE_CLASSES so that it'll get called last during the request phase.
""" """
def __init__(self): def __init__(self):
self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS
self.cache = get_cache(self.cache_alias) self.cache = caches[self.cache_alias]
def process_request(self, request): def process_request(self, request):
""" """
@ -169,39 +168,32 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware):
# we fall back to system defaults. If it is not provided at all, # we fall back to system defaults. If it is not provided at all,
# we need to use middleware defaults. # we need to use middleware defaults.
cache_kwargs = {} try:
key_prefix = kwargs['key_prefix']
if key_prefix is None:
key_prefix = ''
except KeyError:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.key_prefix = key_prefix
try: try:
self.key_prefix = kwargs['key_prefix'] cache_alias = kwargs['cache_alias']
if self.key_prefix is not None: if cache_alias is None:
cache_kwargs['KEY_PREFIX'] = self.key_prefix cache_alias = DEFAULT_CACHE_ALIAS
else:
self.key_prefix = ''
except KeyError: except KeyError:
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX cache_alias = settings.CACHE_MIDDLEWARE_ALIAS
cache_kwargs['KEY_PREFIX'] = self.key_prefix self.cache_alias = cache_alias
try: if cache_timeout is None:
self.cache_alias = kwargs['cache_alias'] cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
if self.cache_alias is None: self.cache_timeout = cache_timeout
self.cache_alias = DEFAULT_CACHE_ALIAS
if cache_timeout is not None:
cache_kwargs['TIMEOUT'] = cache_timeout
except KeyError:
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS
if cache_timeout is None:
cache_kwargs['TIMEOUT'] = settings.CACHE_MIDDLEWARE_SECONDS
else:
cache_kwargs['TIMEOUT'] = cache_timeout
if cache_anonymous_only is None: if cache_anonymous_only is None:
self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False)
else: self.cache_anonymous_only = cache_anonymous_only
self.cache_anonymous_only = cache_anonymous_only
if self.cache_anonymous_only: if self.cache_anonymous_only:
msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8."
warnings.warn(msg, DeprecationWarning, stacklevel=1) warnings.warn(msg, DeprecationWarning, stacklevel=1)
self.cache = get_cache(self.cache_alias, **cache_kwargs) self.cache = create_cache(self.cache_alias)
self.cache_timeout = self.cache.default_timeout

View File

@ -1,13 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.cache import get_cache, InvalidCacheBackendError from django.core.cache import caches, InvalidCacheBackendError
from django.core.cache.utils import make_template_fragment_key from django.core.cache.utils import make_template_fragment_key
from django.template import Library, Node, TemplateSyntaxError, VariableDoesNotExist from django.template import Library, Node, TemplateSyntaxError, VariableDoesNotExist
register = Library() register = Library()
try: try:
default_cache = get_cache('template_fragments') default_cache = caches['template_fragments']
except InvalidCacheBackendError: except InvalidCacheBackendError:
from django.core.cache import cache as default_cache from django.core.cache import cache as default_cache
@ -35,7 +35,7 @@ class CacheNode(Node):
except VariableDoesNotExist: except VariableDoesNotExist:
raise TemplateSyntaxError('"cache" tag got an unknown variable: %r' % self.cache_name.var) raise TemplateSyntaxError('"cache" tag got an unknown variable: %r' % self.cache_name.var)
try: try:
cache = get_cache(cache_name) cache = caches[cache_name]
except InvalidCacheBackendError: except InvalidCacheBackendError:
raise TemplateSyntaxError('Invalid cache name specified for cache tag: %r' % cache_name) raise TemplateSyntaxError('Invalid cache name specified for cache tag: %r' % cache_name)
else: else:

View File

@ -23,7 +23,7 @@ import re
import time import time
from django.conf import settings from django.conf import settings
from django.core.cache import get_cache from django.core.cache import caches
from django.utils.encoding import iri_to_uri, force_bytes, force_text from django.utils.encoding import iri_to_uri, force_bytes, force_text
from django.utils.http import http_date from django.utils.http import http_date
from django.utils.timezone import get_current_timezone_name from django.utils.timezone import get_current_timezone_name
@ -219,7 +219,7 @@ def get_cache_key(request, key_prefix=None, method='GET', cache=None):
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
cache_key = _generate_cache_header_key(key_prefix, request) cache_key = _generate_cache_header_key(key_prefix, request)
if cache is None: if cache is None:
cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
headerlist = cache.get(cache_key, None) headerlist = cache.get(cache_key, None)
if headerlist is not None: if headerlist is not None:
return _generate_cache_key(request, method, headerlist, key_prefix) return _generate_cache_key(request, method, headerlist, key_prefix)
@ -246,7 +246,7 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
cache_key = _generate_cache_header_key(key_prefix, request) cache_key = _generate_cache_header_key(key_prefix, request)
if cache is None: if cache is None:
cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
if response.has_header('Vary'): if response.has_header('Vary'):
is_accept_language_redundant = settings.USE_I18N or settings.USE_L10N is_accept_language_redundant = settings.USE_I18N or settings.USE_L10N
# If i18n or l10n are used, the generated cache key will be suffixed # If i18n or l10n are used, the generated cache key will be suffixed

View File

@ -215,6 +215,9 @@ these changes.
* The internal ``django.utils.functional.memoize`` will be removed. * The internal ``django.utils.functional.memoize`` will be removed.
* ``get_cache`` from django.core.cache will be removed. Instead, use
``create_cache`` or ``caches``, depending on your need.
2.0 2.0
--- ---

View File

@ -269,6 +269,26 @@ Minor features
allowing the ``published`` element to be included in the feed (which allowing the ``published`` element to be included in the feed (which
relies on ``pubdate``). relies on ``pubdate``).
Cache
^^^^^
* Access to caches configured in ``settings.CACHES`` is now available via
``django.core.cache.caches``. This will now return a different instance per
thread.
* A new function ``django.core.cache.create_cache`` has been added to make it
clearer what's happening. ``django.core.cache.get_cache`` will call this
if it's passed anything other than just a cache config alias.
* ``django.core.cache.get_cache`` has been deprecated. Use
``django.core.cache.caches`` to access caches configurd in
``settings.CACHES``, or ``django.core.cache.create_cache`` to create ad-hoc
instances.
* All thread safety in cache backends has been removed, as
``django.core.cache.caches`` now yields differend backend instances per
thread.
Email Email
^^^^^ ^^^^^
@ -643,6 +663,12 @@ Miscellaneous
Features deprecated in 1.7 Features deprecated in 1.7
========================== ==========================
``django.core.cache.get_cache``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``django.core.cache.get_cache`` has been supplanted by
``django.core.cache.caches`` and ``django.core.cache.create_cache``.
``django.utils.dictconfig``/``django.utils.importlib`` ``django.utils.dictconfig``/``django.utils.importlib``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -703,7 +703,22 @@ pickling.)
Accessing the cache Accessing the cache
------------------- -------------------
.. function:: django.core.cache.get_cache(backend, **kwargs) .. versionadded:: 1.7
You can access the caches configured in ``settings.CACHES`` through the
dict-like ``django.core.cache.caches`` object. Repeated requests for the same
alias will return the same object.
>>> from django.core.cache import caches
>>> cache1 = caches['myalias']
>>> cache2 = caches['myalias']
>>> cache1 is cache2
True
If the named key does not exist, ``InvalidCacheBackendError`` will be raised.
The ``caches`` dict is thread aware, so a different instance of each alias will
be returned for each thread.
The cache module, ``django.core.cache``, has a ``cache`` object that's The cache module, ``django.core.cache``, has a ``cache`` object that's
automatically created from the ``'default'`` entry in the :setting:`CACHES` automatically created from the ``'default'`` entry in the :setting:`CACHES`
@ -711,13 +726,43 @@ setting::
>>> from django.core.cache import cache >>> from django.core.cache import cache
If you have multiple caches defined in :setting:`CACHES`, then you can use This is a proxy object to caches['default']. It is provided for backward
:func:`django.core.cache.get_cache` to retrieve a cache object for any key:: compatiblity.
.. function:: django.core.cache.create_cache(backend, **kwargs)
You can create caches from ad-hoc configurations using ``create_cache``.
>>> from django.core.cache import create_cache
# Create an instance of a specific backend
>>> cache = create_cache(
'django.core.cache.backends.memcached.MemcachedCache',
LOCATION='/tmp/memcached.sock'
)
# Create a separate copy of the 'default' cache:
>>> new_default = create_cache('default')
# Create a cache with the same config as 'default', but a different timeout
>>> cache2 = create_cache('default', TIMEOUT=1)
This is guaranteed to always create a new instance.
.. function:: django.core.cache.get_cache(backend, **kwargs)
.. deprecated:: 1.7
This function has been deprecated in favour of ``caches`` and
``create_cache``.
Before Django 1.7 this was the only way to get a cache instance. Now it acts
as a wrapper to ``create_cache``, except in the case where it is passed only a
configured alias, where it will return the cache from ``caches``::
>>> from django.core.cache import get_cache >>> from django.core.cache import get_cache
>>> cache = get_cache('alternate') # Passes call to create_cache
>>> cache = get_cache('django.core.cache.backends.memcached.MemcachedCache', LOCATION='127.0.0.2')
If the named key does not exist, ``InvalidCacheBackendError`` will be raised. # Creates a new cache based on the config in settings.CACHES['default']
>>> cache = get_cache('default', TIMEOUT=300)
# Returns instance from caches object
>>> cache = get_cache('default')
Basic usage Basic usage

118
tests/cache/tests.py vendored
View File

@ -10,13 +10,14 @@ import random
import re import re
import string import string
import tempfile import tempfile
import threading
import time import time
import unittest import unittest
import warnings import warnings
from django.conf import settings from django.conf import settings
from django.core import management from django.core import management
from django.core.cache import get_cache from django.core.cache import create_cache, caches
from django.core.cache.backends.base import (CacheKeyWarning, from django.core.cache.backends.base import (CacheKeyWarning,
InvalidCacheBackendError) InvalidCacheBackendError)
from django.db import connection, router, transaction from django.db import connection, router, transaction
@ -55,7 +56,7 @@ class DummyCacheTests(unittest.TestCase):
backend_name = 'django.core.cache.backends.dummy.DummyCache' backend_name = 'django.core.cache.backends.dummy.DummyCache'
def setUp(self): def setUp(self):
self.cache = get_cache(self.backend_name) self.cache = create_cache(self.backend_name)
def test_simple(self): def test_simple(self):
"Dummy cache backend ignores cache set calls" "Dummy cache backend ignores cache set calls"
@ -840,11 +841,11 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
# Spaces are used in the table name to ensure quoting/escaping is working # Spaces are used in the table name to ensure quoting/escaping is working
self._table_name = 'test cache table' self._table_name = 'test cache table'
management.call_command('createcachetable', verbosity=0, interactive=False) management.call_command('createcachetable', verbosity=0, interactive=False)
self.cache = get_cache(self.backend_name, LOCATION=self._table_name, OPTIONS={'MAX_ENTRIES': 30}) self.cache = create_cache(self.backend_name, LOCATION=self._table_name, OPTIONS={'MAX_ENTRIES': 30})
self.prefix_cache = get_cache(self.backend_name, LOCATION=self._table_name, KEY_PREFIX='cacheprefix') self.prefix_cache = create_cache(self.backend_name, LOCATION=self._table_name, KEY_PREFIX='cacheprefix')
self.v2_cache = get_cache(self.backend_name, LOCATION=self._table_name, VERSION=2) self.v2_cache = create_cache(self.backend_name, LOCATION=self._table_name, VERSION=2)
self.custom_key_cache = get_cache(self.backend_name, LOCATION=self._table_name, KEY_FUNCTION=custom_key_func) self.custom_key_cache = create_cache(self.backend_name, LOCATION=self._table_name, KEY_FUNCTION=custom_key_func)
self.custom_key_cache2 = get_cache(self.backend_name, LOCATION=self._table_name, KEY_FUNCTION='cache.tests.custom_key_func') self.custom_key_cache2 = create_cache(self.backend_name, LOCATION=self._table_name, KEY_FUNCTION='cache.tests.custom_key_func')
def tearDown(self): def tearDown(self):
cursor = connection.cursor() cursor = connection.cursor()
@ -855,7 +856,7 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
self.perform_cull_test(50, 29) self.perform_cull_test(50, 29)
def test_zero_cull(self): def test_zero_cull(self):
self.cache = get_cache(self.backend_name, LOCATION=self._table_name, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0}) self.cache = create_cache(self.backend_name, LOCATION=self._table_name, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0})
self.perform_cull_test(50, 18) self.perform_cull_test(50, 18)
def test_second_call_doesnt_crash(self): def test_second_call_doesnt_crash(self):
@ -950,11 +951,11 @@ class LocMemCacheTests(unittest.TestCase, BaseCacheTests):
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
self.cache = get_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30}) self.cache = create_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30})
self.prefix_cache = get_cache(self.backend_name, KEY_PREFIX='cacheprefix') self.prefix_cache = create_cache(self.backend_name, KEY_PREFIX='cacheprefix')
self.v2_cache = get_cache(self.backend_name, VERSION=2) self.v2_cache = create_cache(self.backend_name, VERSION=2)
self.custom_key_cache = get_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30}, KEY_FUNCTION=custom_key_func) self.custom_key_cache = create_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30}, KEY_FUNCTION=custom_key_func)
self.custom_key_cache2 = get_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30}, KEY_FUNCTION='cache.tests.custom_key_func') self.custom_key_cache2 = create_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30}, KEY_FUNCTION='cache.tests.custom_key_func')
# LocMem requires a hack to make the other caches # LocMem requires a hack to make the other caches
# share a data store with the 'normal' cache. # share a data store with the 'normal' cache.
@ -977,13 +978,13 @@ class LocMemCacheTests(unittest.TestCase, BaseCacheTests):
self.perform_cull_test(50, 29) self.perform_cull_test(50, 29)
def test_zero_cull(self): def test_zero_cull(self):
self.cache = get_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0}) self.cache = create_cache(self.backend_name, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0})
self.perform_cull_test(50, 19) self.perform_cull_test(50, 19)
def test_multiple_caches(self): def test_multiple_caches(self):
"Check that multiple locmem caches are isolated" "Check that multiple locmem caches are isolated"
mirror_cache = get_cache(self.backend_name) mirror_cache = create_cache(self.backend_name)
other_cache = get_cache(self.backend_name, LOCATION='other') other_cache = create_cache(self.backend_name, LOCATION='other')
self.cache.set('value1', 42) self.cache.set('value1', 42)
self.assertEqual(mirror_cache.get('value1'), 42) self.assertEqual(mirror_cache.get('value1'), 42)
@ -1017,11 +1018,11 @@ class MemcachedCacheTests(unittest.TestCase, BaseCacheTests):
if cache['BACKEND'].startswith('django.core.cache.backends.memcached.'): if cache['BACKEND'].startswith('django.core.cache.backends.memcached.'):
break break
random_prefix = ''.join(random.choice(string.ascii_letters) for x in range(10)) random_prefix = ''.join(random.choice(string.ascii_letters) for x in range(10))
self.cache = get_cache(cache_key) self.cache = caches[cache_key]
self.prefix_cache = get_cache(cache_key, KEY_PREFIX=random_prefix) self.prefix_cache = create_cache(cache_key, KEY_PREFIX=random_prefix)
self.v2_cache = get_cache(cache_key, VERSION=2) self.v2_cache = create_cache(cache_key, VERSION=2)
self.custom_key_cache = get_cache(cache_key, KEY_FUNCTION=custom_key_func) self.custom_key_cache = create_cache(cache_key, KEY_FUNCTION=custom_key_func)
self.custom_key_cache2 = get_cache(cache_key, KEY_FUNCTION='cache.tests.custom_key_func') self.custom_key_cache2 = create_cache(cache_key, KEY_FUNCTION='cache.tests.custom_key_func')
def tearDown(self): def tearDown(self):
self.cache.clear() self.cache.clear()
@ -1050,7 +1051,7 @@ class MemcachedCacheTests(unittest.TestCase, BaseCacheTests):
# Regression test for #19810 # Regression test for #19810
for cache_key, cache in settings.CACHES.items(): for cache_key, cache in settings.CACHES.items():
if cache['BACKEND'] == 'django.core.cache.backends.memcached.MemcachedCache': if cache['BACKEND'] == 'django.core.cache.backends.memcached.MemcachedCache':
self.assertEqual(get_cache(cache_key)._cache.pickleProtocol, self.assertEqual(caches[cache_key]._cache.pickleProtocol,
pickle.HIGHEST_PROTOCOL) pickle.HIGHEST_PROTOCOL)
@ -1063,11 +1064,11 @@ class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
self.dirname = tempfile.mkdtemp() self.dirname = tempfile.mkdtemp()
self.cache = get_cache(self.backend_name, LOCATION=self.dirname, OPTIONS={'MAX_ENTRIES': 30}) self.cache = create_cache(self.backend_name, LOCATION=self.dirname, OPTIONS={'MAX_ENTRIES': 30})
self.prefix_cache = get_cache(self.backend_name, LOCATION=self.dirname, KEY_PREFIX='cacheprefix') self.prefix_cache = create_cache(self.backend_name, LOCATION=self.dirname, KEY_PREFIX='cacheprefix')
self.v2_cache = get_cache(self.backend_name, LOCATION=self.dirname, VERSION=2) self.v2_cache = create_cache(self.backend_name, LOCATION=self.dirname, VERSION=2)
self.custom_key_cache = get_cache(self.backend_name, LOCATION=self.dirname, KEY_FUNCTION=custom_key_func) self.custom_key_cache = create_cache(self.backend_name, LOCATION=self.dirname, KEY_FUNCTION=custom_key_func)
self.custom_key_cache2 = get_cache(self.backend_name, LOCATION=self.dirname, KEY_FUNCTION='cache.tests.custom_key_func') self.custom_key_cache2 = create_cache(self.backend_name, LOCATION=self.dirname, KEY_FUNCTION='cache.tests.custom_key_func')
def tearDown(self): def tearDown(self):
self.cache.clear() self.cache.clear()
@ -1097,7 +1098,7 @@ class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
def test_zero_cull(self): def test_zero_cull(self):
# Regression test for #15806 # Regression test for #15806
self.cache = get_cache(self.backend_name, LOCATION=self.dirname, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0}) self.cache = create_cache(self.backend_name, LOCATION=self.dirname, OPTIONS={'MAX_ENTRIES': 30, 'CULL_FREQUENCY': 0})
self.perform_cull_test(50, 19) self.perform_cull_test(50, 19)
@ -1109,7 +1110,7 @@ class CustomCacheKeyValidationTests(unittest.TestCase):
""" """
def test_custom_key_validation(self): def test_custom_key_validation(self):
cache = get_cache('cache.liberal_backend.CacheClass') cache = create_cache('cache.liberal_backend.CacheClass')
# this key is both longer than 250 characters, and has spaces # this key is both longer than 250 characters, and has spaces
key = 'some key with spaces' * 15 key = 'some key with spaces' * 15
@ -1121,18 +1122,23 @@ class CustomCacheKeyValidationTests(unittest.TestCase):
class GetCacheTests(unittest.TestCase): class GetCacheTests(unittest.TestCase):
def test_simple(self): def test_simple(self):
from django.core.cache import cache from django.core.cache import caches, DEFAULT_CACHE_ALIAS
self.assertIsInstance(cache, get_cache('default').__class__) self.assertIsInstance(
caches[DEFAULT_CACHE_ALIAS],
create_cache('default').__class__
)
cache = get_cache( cache = create_cache(
'django.core.cache.backends.dummy.DummyCache', **{'TIMEOUT': 120}) 'django.core.cache.backends.dummy.DummyCache',
**{'TIMEOUT': 120}
)
self.assertEqual(cache.default_timeout, 120) self.assertEqual(cache.default_timeout, 120)
self.assertRaises(InvalidCacheBackendError, get_cache, 'does_not_exist') self.assertRaises(InvalidCacheBackendError, create_cache, 'does_not_exist')
def test_close(self): def test_close(self):
from django.core import signals from django.core import signals
cache = get_cache('cache.closeable_cache.CacheClass') cache = create_cache('cache.closeable_cache.CacheClass')
self.assertFalse(cache.closed) self.assertFalse(cache.closed)
signals.request_finished.send(self.__class__) signals.request_finished.send(self.__class__)
self.assertTrue(cache.closed) self.assertTrue(cache.closed)
@ -1153,7 +1159,7 @@ class CacheUtils(TestCase):
def setUp(self): def setUp(self):
self.path = '/cache/test/' self.path = '/cache/test/'
self.cache = get_cache('default') self.cache = caches['default']
self.factory = RequestFactory() self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
@ -1261,7 +1267,7 @@ class CacheHEADTest(TestCase):
def setUp(self): def setUp(self):
self.path = '/cache/test/' self.path = '/cache/test/'
self.cache = get_cache('default') self.cache = caches['default']
self.factory = RequestFactory() self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
@ -1314,7 +1320,7 @@ class CacheI18nTest(TestCase):
def setUp(self): def setUp(self):
self.path = '/cache/test/' self.path = '/cache/test/'
self.cache = get_cache('default') self.cache = create_cache('default')
self.factory = RequestFactory() self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
@ -1581,8 +1587,8 @@ class CacheMiddlewareTest(IgnoreDeprecationWarningsMixin, TestCase):
def setUp(self): def setUp(self):
super(CacheMiddlewareTest, self).setUp() super(CacheMiddlewareTest, self).setUp()
self.factory = RequestFactory() self.factory = RequestFactory()
self.default_cache = get_cache('default') self.default_cache = create_cache('default')
self.other_cache = get_cache('other') self.other_cache = create_cache('other')
def tearDown(self): def tearDown(self):
self.default_cache.clear() self.default_cache.clear()
@ -1608,7 +1614,7 @@ class CacheMiddlewareTest(IgnoreDeprecationWarningsMixin, TestCase):
# First, test with "defaults": # First, test with "defaults":
as_view_decorator = CacheMiddleware(cache_alias=None, key_prefix=None) as_view_decorator = CacheMiddleware(cache_alias=None, key_prefix=None)
self.assertEqual(as_view_decorator.cache_timeout, 300) # Timeout value for 'default' cache, i.e. 300 self.assertEqual(as_view_decorator.cache_timeout, 30) # Timeout value for 'default' cache, i.e. 30
self.assertEqual(as_view_decorator.key_prefix, '') self.assertEqual(as_view_decorator.key_prefix, '')
self.assertEqual(as_view_decorator.cache_alias, 'default') # Value of DEFAULT_CACHE_ALIAS from django.core.cache self.assertEqual(as_view_decorator.cache_alias, 'default') # Value of DEFAULT_CACHE_ALIAS from django.core.cache
self.assertEqual(as_view_decorator.cache_anonymous_only, False) self.assertEqual(as_view_decorator.cache_anonymous_only, False)
@ -1755,7 +1761,7 @@ class CacheMiddlewareTest(IgnoreDeprecationWarningsMixin, TestCase):
time.sleep(2) time.sleep(2)
# ... the default cache will still hit # ... the default cache will still hit
get_cache('default') caches['default']
response = default_view(request, '11') response = default_view(request, '11')
self.assertEqual(response.content, b'Hello World 1') self.assertEqual(response.content, b'Hello World 1')
@ -1801,7 +1807,7 @@ class TestWithTemplateResponse(TestCase):
""" """
def setUp(self): def setUp(self):
self.path = '/cache/test/' self.path = '/cache/test/'
self.cache = get_cache('default') self.cache = create_cache('default')
self.factory = RequestFactory() self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
@ -1904,3 +1910,29 @@ class TestMakeTemplateFragmentKey(TestCase):
key = make_template_fragment_key('spam', ['abc:def%']) key = make_template_fragment_key('spam', ['abc:def%'])
self.assertEqual(key, self.assertEqual(key,
'template.cache.spam.f27688177baec990cdf3fbd9d9c3f469') 'template.cache.spam.f27688177baec990cdf3fbd9d9c3f469')
class CacheHandlerTest(TestCase):
def test_same_instance(self):
"""
Attempting to retrieve the same alias should yield the same instance.
"""
cache1 = caches['default']
cache2 = caches['default']
self.assertTrue(cache1 is cache2)
def test_per_thread(self):
"""
Requesting the same alias from separate threads should yield separate
instances.
"""
c = []
def runner():
c.append(caches['default'])
for x in range(2):
t = threading.Thread(target=runner)
t.start()
t.join()
self.assertFalse(c[0] is c[1])