diff options
| author | Curtis Maloney <curtis@tinbrain.net> | 2013-10-19 09:49:24 +1100 |
|---|---|---|
| committer | Aymeric Augustin <aymeric.augustin@m4x.org> | 2013-11-23 15:06:59 +0100 |
| commit | ffc37e2343a93cf6d44247e20cd263b41f931716 (patch) | |
| tree | ce16f3cc212271f3a4d9f02d25c87b770f09955e /django | |
| parent | 3ca0815c0b4ddf7dd1fe74839e2c3e8633c3ea31 (diff) | |
Fixed #21012 -- New API to access cache backends.
Thanks Curtis Malony and Florian Apolloner.
Squashed commit of the following:
commit 3380495e93f5e81b80a251b03ddb0a80b17685f5
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Sat Nov 23 14:18:07 2013 +0100
Looked up the template_fragments cache at runtime.
commit 905a74f52b24a198f802520ff06290a94dedc687
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Sat Nov 23 14:19:48 2013 +0100
Removed all uses of create_cache.
Refactored the cache tests significantly.
Made it safe to override the CACHES setting.
commit 35e289fe9285feffed3c60657af9279a6a2cfccc
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Sat Nov 23 12:23:57 2013 +0100
Removed create_cache function.
commit 8e274f747a1f1c0c0e6c37873e29067f7fa022e8
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Sat Nov 23 12:04:52 2013 +0100
Updated docs to describe a simplified cache backend API.
commit ee7eb0f73e6d4699edcf5d357dce715224525cf6
Author: Curtis Maloney <curtis@tinbrain.net>
Date: Sat Oct 19 09:49:24 2013 +1100
Fixed #21012 -- Thread-local caches, like databases.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/sessions/backends/cache.py | 4 | ||||
| -rw-r--r-- | django/contrib/sessions/backends/cached_db.py | 4 | ||||
| -rw-r--r-- | django/contrib/sessions/tests.py | 11 | ||||
| -rw-r--r-- | django/contrib/staticfiles/storage.py | 4 | ||||
| -rw-r--r-- | django/core/cache/__init__.py | 76 | ||||
| -rw-r--r-- | django/core/cache/backends/memcached.py | 14 | ||||
| -rw-r--r-- | django/core/management/commands/createcachetable.py | 4 | ||||
| -rw-r--r-- | django/middleware/cache.py | 48 | ||||
| -rw-r--r-- | django/templatetags/cache.py | 19 | ||||
| -rw-r--r-- | django/test/signals.py | 8 | ||||
| -rw-r--r-- | django/utils/cache.py | 6 |
11 files changed, 124 insertions, 74 deletions
diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py index 596042fcb3..b1058b3294 100644 --- a/django/contrib/sessions/backends/cache.py +++ b/django/contrib/sessions/backends/cache.py @@ -1,6 +1,6 @@ from django.conf import settings 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 KEY_PREFIX = "django.contrib.sessions.cache" @@ -11,7 +11,7 @@ class SessionStore(SessionBase): A cache-based session store. """ 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) @property diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py index 15b7172f85..f5c14b0e1e 100644 --- a/django/contrib/sessions/backends/cached_db.py +++ b/django/contrib/sessions/backends/cached_db.py @@ -6,7 +6,7 @@ import logging from django.conf import settings 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.utils import timezone from django.utils.encoding import force_text @@ -20,7 +20,7 @@ class SessionStore(DBStore): """ 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) @property diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 862bdfb69b..af89f0048e 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -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.models import Session 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 import management from django.core.exceptions import ImproperlyConfigured @@ -140,7 +140,7 @@ class SessionTestsMixin(object): self.assertTrue(self.session.modified) 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']): raise unittest.SkipTest("Session saving tests require a real cache backend") self.session.save() @@ -481,7 +481,7 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): def test_default_cache(self): 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={ 'default': { @@ -489,6 +489,7 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): }, 'sessions': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'session', }, }, SESSION_CACHE_ALIAS='sessions') def test_non_default_cache(self): @@ -496,8 +497,8 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): self.session = self.backend() self.session.save() - self.assertEqual(get_cache('default').get(self.session.cache_key), None) - self.assertNotEqual(get_cache('sessions').get(self.session.cache_key), None) + self.assertEqual(caches['default'].get(self.session.cache_key), None) + self.assertNotEqual(caches['sessions'].get(self.session.cache_key), None) class SessionMiddlewareTests(unittest.TestCase): diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index a527379feb..5190a299e4 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -7,7 +7,7 @@ import posixpath import re from django.conf import settings -from django.core.cache import (get_cache, InvalidCacheBackendError, +from django.core.cache import (caches, InvalidCacheBackendError, cache as default_cache) from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile @@ -56,7 +56,7 @@ class CachedFilesMixin(object): def __init__(self, *args, **kwargs): super(CachedFilesMixin, self).__init__(*args, **kwargs) try: - self.cache = get_cache('staticfiles') + self.cache = caches['staticfiles'] except InvalidCacheBackendError: # Use the default backend self.cache = default_cache diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py index 747a12a023..6829b74772 100644 --- a/django/core/cache/__init__.py +++ b/django/core/cache/__init__.py @@ -6,14 +6,15 @@ In a nutshell, a cache is a set of values -- which can be any object that may be pickled -- identified by string keys. For the complete API, see the abstract BaseCache class in django.core.cache.backends.base. -Client code should not access a cache backend directly; instead it should -either use the "cache" variable made available here, or it should use the -get_cache() function made available here. get_cache() takes a CACHES alias or a -backend path and config parameters, and returns an instance of a backend cache -class. +Client code should use the `cache` variable defined here to access the default +cache backend and look up non-default cache backends in the `caches` dict-like +object. See docs/topics/cache.txt for information on the public API. """ +from threading import local +import warnings + from django.conf import settings from django.core import signals from django.core.cache.backends.base import ( @@ -35,14 +36,14 @@ if DEFAULT_CACHE_ALIAS not in settings.CACHES: def get_cache(backend, **kwargs): """ - Function to load a cache backend dynamically. This is flexible by design + Function to create a cache backend dynamically. This is flexible by design to allow different use cases: To load a backend that is pre-defined in the settings:: cache = get_cache('default') - To load a backend with its dotted import path, + To create a backend with its dotted import path, including arbitrary options:: cache = get_cache('django.core.cache.backends.memcached.MemcachedCache', **{ @@ -50,6 +51,12 @@ def get_cache(backend, **kwargs): }) """ + warnings.warn("'get_cache' is deprecated in favor of 'caches'.", + PendingDeprecationWarning, stacklevel=2) + return _create_cache(backend, **kwargs) + + +def _create_cache(backend, **kwargs): try: # Try to get the CACHES entry for the given backend name first try: @@ -79,4 +86,57 @@ def get_cache(backend, **kwargs): signals.request_finished.connect(cache.close) 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 __contains__(self, key): + return key in caches[DEFAULT_CACHE_ALIAS] + + def __eq__(self, other): + return caches[DEFAULT_CACHE_ALIAS] == other + + def __ne__(self, other): + return caches[DEFAULT_CACHE_ALIAS] != other + +cache = DefaultCacheProxy() diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index c49c20e59b..216ceb04e1 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -2,13 +2,13 @@ import time import pickle -from threading import local from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils import six from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import force_str +from django.utils.functional import cached_property class BaseMemcachedCacheMethods(RenameMethodsBase): @@ -177,24 +177,14 @@ class PyLibMCCache(BaseMemcachedCache): "An implementation of a cache binding using pylibmc" def __init__(self, server, params): import pylibmc - self._local = local() super(PyLibMCCache, self).__init__(server, params, library=pylibmc, value_not_found_exception=pylibmc.NotFound) - @property + @cached_property 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) if self._options: client.behaviors = self._options - self._local.client = client - return client diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index be6da1dbf0..a71ff945a9 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -1,7 +1,7 @@ from optparse import make_option from django.conf import settings -from django.core.cache import get_cache +from django.core.cache import caches from django.core.cache.backends.db import BaseDatabaseCache from django.core.management.base import BaseCommand, CommandError from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS @@ -30,7 +30,7 @@ class Command(BaseCommand): self.create_table(db, tablename) else: for cache_alias in settings.CACHES: - cache = get_cache(cache_alias) + cache = caches[cache_alias] if isinstance(cache, BaseDatabaseCache): self.create_table(db, cache._table) diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 361e46f5e5..a99d428ad9 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -46,7 +46,7 @@ More details about how the caching works: import warnings from django.conf import settings -from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS +from django.core.cache import caches, DEFAULT_CACHE_ALIAS 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.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) 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): try: @@ -122,10 +122,9 @@ class FetchFromCacheMiddleware(object): MIDDLEWARE_CLASSES so that it'll get called last during the request phase. """ def __init__(self): - self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX 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): """ @@ -169,39 +168,32 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): # we fall back to system defaults. If it is not provided at all, # we need to use middleware defaults. - cache_kwargs = {} - try: - self.key_prefix = kwargs['key_prefix'] - if self.key_prefix is not None: - cache_kwargs['KEY_PREFIX'] = self.key_prefix - else: - self.key_prefix = '' + key_prefix = kwargs['key_prefix'] + if key_prefix is None: + key_prefix = '' except KeyError: - self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX - cache_kwargs['KEY_PREFIX'] = self.key_prefix + key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + self.key_prefix = key_prefix try: - self.cache_alias = kwargs['cache_alias'] - if self.cache_alias is None: - self.cache_alias = DEFAULT_CACHE_ALIAS - if cache_timeout is not None: - cache_kwargs['TIMEOUT'] = cache_timeout + cache_alias = kwargs['cache_alias'] + if cache_alias is None: + cache_alias = DEFAULT_CACHE_ALIAS 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 + cache_alias = settings.CACHE_MIDDLEWARE_ALIAS + self.cache_alias = cache_alias + + if cache_timeout is None: + cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.cache_timeout = cache_timeout if cache_anonymous_only is None: - self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) - else: - self.cache_anonymous_only = cache_anonymous_only + cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) + self.cache_anonymous_only = cache_anonymous_only if self.cache_anonymous_only: msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." warnings.warn(msg, DeprecationWarning, stacklevel=1) - self.cache = get_cache(self.cache_alias, **cache_kwargs) - self.cache_timeout = self.cache.default_timeout + self.cache = caches[self.cache_alias] diff --git a/django/templatetags/cache.py b/django/templatetags/cache.py index 1ef97d7cf0..bdea9b59ae 100644 --- a/django/templatetags/cache.py +++ b/django/templatetags/cache.py @@ -1,16 +1,11 @@ from __future__ import unicode_literals -from django.core.cache import get_cache, InvalidCacheBackendError +from django.core.cache import cache, caches, InvalidCacheBackendError from django.core.cache.utils import make_template_fragment_key from django.template import Library, Node, TemplateSyntaxError, VariableDoesNotExist register = Library() -try: - default_cache = get_cache('template_fragments') -except InvalidCacheBackendError: - from django.core.cache import cache as default_cache - class CacheNode(Node): def __init__(self, nodelist, expire_time_var, fragment_name, vary_on, cache_name): @@ -35,17 +30,21 @@ class CacheNode(Node): except VariableDoesNotExist: raise TemplateSyntaxError('"cache" tag got an unknown variable: %r' % self.cache_name.var) try: - cache = get_cache(cache_name) + fragment_cache = caches[cache_name] except InvalidCacheBackendError: raise TemplateSyntaxError('Invalid cache name specified for cache tag: %r' % cache_name) else: - cache = default_cache + try: + fragment_cache = caches['template_fragments'] + except InvalidCacheBackendError: + fragment_cache = caches['default'] + vary_on = [var.resolve(context) for var in self.vary_on] cache_key = make_template_fragment_key(self.fragment_name, vary_on) - value = cache.get(cache_key) + value = fragment_cache.get(cache_key) if value is None: value = self.nodelist.render(context) - cache.set(cache_key, value, expire_time) + fragment_cache.set(cache_key, value, expire_time) return value diff --git a/django/test/signals.py b/django/test/signals.py index de8802b381..4af408ea9d 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -1,5 +1,6 @@ import os import time +import threading import warnings from django.conf import settings @@ -20,6 +21,13 @@ COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES']) @receiver(setting_changed) +def clear_cache_handlers(**kwargs): + if kwargs['setting'] == 'CACHES': + from django.core.cache import caches + caches._caches = threading.local() + + +@receiver(setting_changed) def update_connections_time_zone(**kwargs): if kwargs['setting'] == 'TIME_ZONE': # Reset process time zone diff --git a/django/utils/cache.py b/django/utils/cache.py index 7e38f7b021..d89408a7a2 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -23,7 +23,7 @@ import re import time 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.http import http_date 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 cache_key = _generate_cache_header_key(key_prefix, request) if cache is None: - cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) + cache = caches[settings.CACHE_MIDDLEWARE_ALIAS] headerlist = cache.get(cache_key, None) if headerlist is not None: 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_key = _generate_cache_header_key(key_prefix, request) if cache is None: - cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) + cache = caches[settings.CACHE_MIDDLEWARE_ALIAS] if response.has_header('Vary'): is_accept_language_redundant = settings.USE_I18N or settings.USE_L10N # If i18n or l10n are used, the generated cache key will be suffixed |
