From bf757a2f4dad519fac1b4a458376de3a040f5ca8 Mon Sep 17 00:00:00 2001
From: Michael Manfre <mmanfre@gmail.com>
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.