From bf757a2f4dad519fac1b4a458376de3a040f5ca8 Mon Sep 17 00:00:00 2001 From: Michael Manfre Date: Mon, 23 Sep 2013 12:40:19 -0400 Subject: [PATCH] Fixed #21147 -- Avoided time.time precision issue with cache backends. The precision of time.time() is OS specific and it is possible for the resolution to be low enough to allow reading a cache key previously set with a timeout of 0. --- django/core/cache/backends/base.py | 13 +++++++++++++ django/core/cache/backends/db.py | 7 +++---- django/core/cache/backends/filebased.py | 5 +---- django/core/cache/backends/locmem.py | 5 +---- django/core/cache/backends/memcached.py | 18 +++++++++++++----- docs/internals/deprecation.txt | 4 ++++ docs/releases/1.7.txt | 6 ++++++ 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index db76192354..13c34ea697 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -1,6 +1,7 @@ "Base Cache class." from __future__ import unicode_literals +import time import warnings from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning @@ -74,6 +75,18 @@ class BaseCache(object): self.version = params.get('VERSION', 1) self.key_func = get_key_func(params.get('KEY_FUNCTION', None)) + def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): + """ + Returns the timeout value usable by this backend based upon the provided + timeout. + """ + if timeout == DEFAULT_TIMEOUT: + timeout = self.default_timeout + elif timeout == 0: + # ticket 21147 - avoid time.time() related precision issues + timeout = -1 + return None if timeout is None else time.time() + timeout + def make_key(self, key, version=None): """Constructs the key used by all other methods. By default it uses the key_func to generate a key (which, by default, diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 8844bc8f61..40d1320e37 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -92,8 +92,7 @@ class DatabaseCache(BaseDatabaseCache): return self._base_set('add', key, value, timeout) def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT): - if timeout == DEFAULT_TIMEOUT: - timeout = self.default_timeout + timeout = self.get_backend_timeout(timeout) db = router.db_for_write(self.cache_model_class) table = connections[db].ops.quote_name(self._table) cursor = connections[db].cursor() @@ -105,9 +104,9 @@ class DatabaseCache(BaseDatabaseCache): if timeout is None: exp = datetime.max elif settings.USE_TZ: - exp = datetime.utcfromtimestamp(time.time() + timeout) + exp = datetime.utcfromtimestamp(timeout) else: - exp = datetime.fromtimestamp(time.time() + timeout) + exp = datetime.fromtimestamp(timeout) exp = exp.replace(microsecond=0) if num > self._max_entries: self._cull(db, cursor, now) diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index d19eed4a95..83ded57034 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -51,9 +51,6 @@ class FileBasedCache(BaseCache): fname = self._key_to_file(key) dirname = os.path.dirname(fname) - if timeout == DEFAULT_TIMEOUT: - timeout = self.default_timeout - self._cull() try: @@ -61,7 +58,7 @@ class FileBasedCache(BaseCache): os.makedirs(dirname) with open(fname, 'wb') as f: - expiry = None if timeout is None else time.time() + timeout + expiry = self.get_backend_timeout(timeout) pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL) pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) except (IOError, OSError): diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index 44d7db62c3..cab690bb0c 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -63,11 +63,8 @@ class LocMemCache(BaseCache): def _set(self, key, value, timeout=DEFAULT_TIMEOUT): if len(self._cache) >= self._max_entries: self._cull() - if timeout == DEFAULT_TIMEOUT: - timeout = self.default_timeout - expiry = None if timeout is None else time.time() + timeout self._cache[key] = value - self._expire_info[key] = expiry + self._expire_info[key] = self.get_backend_timeout(timeout) def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index 19e8b02f74..edb756c365 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -7,9 +7,17 @@ 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 -class BaseMemcachedCache(BaseCache): + +class BaseMemcachedCacheMethods(RenameMethodsBase): + renamed_methods = ( + ('_get_memcache_timeout', 'get_backend_timeout', PendingDeprecationWarning), + ) + + +class BaseMemcachedCache(six.with_metaclass(BaseMemcachedCacheMethods, BaseCache)): def __init__(self, server, params, library, value_not_found_exception): super(BaseMemcachedCache, self).__init__(params) if isinstance(server, six.string_types): @@ -36,7 +44,7 @@ class BaseMemcachedCache(BaseCache): return self._client - def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT): + def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): """ Memcached deals with long (> 30 days) timeouts in a special way. Call this function to obtain a safe value for your timeout. @@ -68,7 +76,7 @@ class BaseMemcachedCache(BaseCache): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) - return self._cache.add(key, value, self._get_memcache_timeout(timeout)) + return self._cache.add(key, value, self.get_backend_timeout(timeout)) def get(self, key, default=None, version=None): key = self.make_key(key, version=version) @@ -79,7 +87,7 @@ class BaseMemcachedCache(BaseCache): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) - self._cache.set(key, value, self._get_memcache_timeout(timeout)) + self._cache.set(key, value, self.get_backend_timeout(timeout)) def delete(self, key, version=None): key = self.make_key(key, version=version) @@ -140,7 +148,7 @@ class BaseMemcachedCache(BaseCache): for key, value in data.items(): key = self.make_key(key, version=version) safe_data[key] = value - self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout)) + self._cache.set_multi(safe_data, self.get_backend_timeout(timeout)) def delete_many(self, keys, version=None): l = lambda x: self.make_key(x, version=version) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 5c1b1cab6f..498f303daf 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -457,6 +457,10 @@ these changes. * ``ModelAdmin.get_formsets`` will be removed. +* Remove the backward compatible shims introduced to rename the + ``BaseMemcachedCache._get_memcache_timeout()`` method to + ``get_backend_timeout()``. + 2.0 --- diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 74fc74947c..a978bb8ac1 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -552,3 +552,9 @@ The :class:`django.db.models.IPAddressField` and :class:`django.db.models.GenericIPAddressField` and :class:`django.forms.GenericIPAddressField`. +``BaseMemcachedCache._get_memcache_timeout`` method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``BaseMemcachedCache._get_memcache_timeout()`` method has been renamed to +``get_backend_timeout()``. Despite being a private API, it will go through the +normal deprecation.