Fixed #9595 -- Allow non-expiring cache timeouts.

Also, streamline the use of 0 and None between cache backends.
This commit is contained in:
Jacob Burch 2013-05-18 12:54:59 +02:00 committed by Aymeric Augustin
parent e0df647143
commit 89955cc35f
10 changed files with 97 additions and 40 deletions

View File

@ -123,6 +123,7 @@ answer newbie questions, and generally made Django that much better:
bthomas bthomas
btoll@bestweb.net btoll@bestweb.net
Jonathan Buchanan <jonathan.buchanan@gmail.com> Jonathan Buchanan <jonathan.buchanan@gmail.com>
Jacob Burch <jacobburch@gmail.com>
Keith Bussell <kbussell@gmail.com> Keith Bussell <kbussell@gmail.com>
C8E C8E
Chris Cahoon <chris.cahoon@gmail.com> Chris Cahoon <chris.cahoon@gmail.com>

View File

@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning):
pass pass
# Stub class to ensure not passing in a `timeout` argument results in
# the default timeout
DEFAULT_TIMEOUT = object()
# Memcached does not accept keys longer than this. # Memcached does not accept keys longer than this.
MEMCACHE_MAX_KEY_LENGTH = 250 MEMCACHE_MAX_KEY_LENGTH = 250
@ -84,7 +88,7 @@ class BaseCache(object):
new_key = self.key_func(key, self.key_prefix, version) new_key = self.key_func(key, self.key_prefix, version)
return new_key return new_key
def add(self, key, value, timeout=None, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
""" """
Set a value in the cache if the key does not already exist. If Set a value in the cache if the key does not already exist. If
timeout is given, that timeout will be used for the key; otherwise timeout is given, that timeout will be used for the key; otherwise
@ -101,7 +105,7 @@ class BaseCache(object):
""" """
raise NotImplementedError raise NotImplementedError
def set(self, key, value, timeout=None, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
""" """
Set a value in the cache. If timeout is given, that timeout will be Set a value in the cache. If timeout is given, that timeout will be
used for the key; otherwise the default cache timeout will be used. used for the key; otherwise the default cache timeout will be used.
@ -163,7 +167,7 @@ class BaseCache(object):
# if a subclass overrides it. # if a subclass overrides it.
return self.has_key(key) return self.has_key(key)
def set_many(self, data, timeout=None, version=None): def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
""" """
Set a bunch of values in the cache at once from a dict of key/value Set a bunch of values in the cache at once from a dict of key/value
pairs. For certain backends (memcached), this is much more efficient pairs. For certain backends (memcached), this is much more efficient

View File

@ -9,7 +9,7 @@ except ImportError:
import pickle import pickle
from django.conf import settings from django.conf import settings
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
from django.db import connections, transaction, router, DatabaseError from django.db import connections, transaction, router, DatabaseError
from django.utils import timezone, six from django.utils import timezone, six
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache):
if row is None: if row is None:
return default return default
now = timezone.now() now = timezone.now()
if row[2] < now: if row[2] < now:
db = router.db_for_write(self.cache_model_class) db = router.db_for_write(self.cache_model_class)
cursor = connections[db].cursor() cursor = connections[db].cursor()
@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache):
value = connections[db].ops.process_clob(row[1]) value = connections[db].ops.process_clob(row[1])
return pickle.loads(base64.b64decode(force_bytes(value))) return pickle.loads(base64.b64decode(force_bytes(value)))
def set(self, key, value, timeout=None, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
self._base_set('set', key, value, timeout) self._base_set('set', key, value, timeout)
def add(self, key, value, timeout=None, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
return self._base_set('add', key, value, timeout) return self._base_set('add', key, value, timeout)
def _base_set(self, mode, key, value, timeout=None): def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT):
if timeout is None: if timeout == DEFAULT_TIMEOUT:
timeout = self.default_timeout timeout = self.default_timeout
db = router.db_for_write(self.cache_model_class) db = router.db_for_write(self.cache_model_class)
table = connections[db].ops.quote_name(self._table) table = connections[db].ops.quote_name(self._table)
@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache):
num = cursor.fetchone()[0] num = cursor.fetchone()[0]
now = timezone.now() now = timezone.now()
now = now.replace(microsecond=0) now = now.replace(microsecond=0)
if settings.USE_TZ: if timeout is None:
exp = datetime.max
elif settings.USE_TZ:
exp = datetime.utcfromtimestamp(time.time() + timeout) exp = datetime.utcfromtimestamp(time.time() + timeout)
else: else:
exp = datetime.fromtimestamp(time.time() + timeout) exp = datetime.fromtimestamp(time.time() + timeout)

View File

@ -1,12 +1,12 @@
"Dummy cache backend" "Dummy cache backend"
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
class DummyCache(BaseCache): class DummyCache(BaseCache):
def __init__(self, host, *args, **kwargs): def __init__(self, host, *args, **kwargs):
BaseCache.__init__(self, *args, **kwargs) BaseCache.__init__(self, *args, **kwargs)
def add(self, key, value, timeout=None, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
return True return True
@ -16,7 +16,7 @@ class DummyCache(BaseCache):
self.validate_key(key) self.validate_key(key)
return default return default
def set(self, key, value, timeout=None, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
@ -32,7 +32,7 @@ class DummyCache(BaseCache):
self.validate_key(key) self.validate_key(key)
return False return False
def set_many(self, data, timeout=0, version=None): def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
pass pass
def delete_many(self, keys, version=None): def delete_many(self, keys, version=None):

View File

@ -9,9 +9,10 @@ try:
except ImportError: except ImportError:
import pickle import pickle
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
class FileBasedCache(BaseCache): class FileBasedCache(BaseCache):
def __init__(self, dir, params): def __init__(self, dir, params):
BaseCache.__init__(self, params) BaseCache.__init__(self, params)
@ -19,7 +20,7 @@ class FileBasedCache(BaseCache):
if not os.path.exists(self._dir): if not os.path.exists(self._dir):
self._createdir() self._createdir()
def add(self, key, value, timeout=None, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
if self.has_key(key, version=version): if self.has_key(key, version=version):
return False return False
@ -35,7 +36,7 @@ class FileBasedCache(BaseCache):
with open(fname, 'rb') as f: with open(fname, 'rb') as f:
exp = pickle.load(f) exp = pickle.load(f)
now = time.time() now = time.time()
if exp < now: if exp is not None and exp < now:
self._delete(fname) self._delete(fname)
else: else:
return pickle.load(f) return pickle.load(f)
@ -43,14 +44,14 @@ class FileBasedCache(BaseCache):
pass pass
return default return default
def set(self, key, value, timeout=None, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
fname = self._key_to_file(key) fname = self._key_to_file(key)
dirname = os.path.dirname(fname) dirname = os.path.dirname(fname)
if timeout is None: if timeout == DEFAULT_TIMEOUT:
timeout = self.default_timeout timeout = self.default_timeout
self._cull() self._cull()
@ -60,8 +61,8 @@ class FileBasedCache(BaseCache):
os.makedirs(dirname) os.makedirs(dirname)
with open(fname, 'wb') as f: with open(fname, 'wb') as f:
now = time.time() expiry = None if timeout is None else time.time() + timeout
pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL) pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL)
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
except (IOError, OSError): except (IOError, OSError):
pass pass

View File

@ -6,7 +6,7 @@ try:
except ImportError: except ImportError:
import pickle import pickle
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
from django.utils.synch import RWLock from django.utils.synch import RWLock
# Global in-memory store of cache data. Keyed by name, to provide # Global in-memory store of cache data. Keyed by name, to provide
@ -23,7 +23,7 @@ class LocMemCache(BaseCache):
self._expire_info = _expire_info.setdefault(name, {}) self._expire_info = _expire_info.setdefault(name, {})
self._lock = _locks.setdefault(name, RWLock()) self._lock = _locks.setdefault(name, RWLock())
def add(self, key, value, timeout=None, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
with self._lock.writer(): with self._lock.writer():
@ -41,10 +41,8 @@ class LocMemCache(BaseCache):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
with self._lock.reader(): with self._lock.reader():
exp = self._expire_info.get(key) exp = self._expire_info.get(key, 0)
if exp is None: if exp is None or exp > time.time():
return default
elif exp > time.time():
try: try:
pickled = self._cache[key] pickled = self._cache[key]
return pickle.loads(pickled) return pickle.loads(pickled)
@ -58,15 +56,16 @@ class LocMemCache(BaseCache):
pass pass
return default return default
def _set(self, key, value, timeout=None): def _set(self, key, value, timeout=DEFAULT_TIMEOUT):
if len(self._cache) >= self._max_entries: if len(self._cache) >= self._max_entries:
self._cull() self._cull()
if timeout is None: if timeout == DEFAULT_TIMEOUT:
timeout = self.default_timeout timeout = self.default_timeout
expiry = None if timeout is None else time.time() + timeout
self._cache[key] = value self._cache[key] = value
self._expire_info[key] = time.time() + timeout self._expire_info[key] = expiry
def set(self, key, value, timeout=None, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self.validate_key(key) self.validate_key(key)
with self._lock.writer(): with self._lock.writer():

View File

@ -4,7 +4,7 @@ import time
import pickle import pickle
from threading import local from threading import local
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
from django.utils import six from django.utils import six
from django.utils.encoding import force_str from django.utils.encoding import force_str
@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache):
return self._client return self._client
def _get_memcache_timeout(self, timeout): def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT):
""" """
Memcached deals with long (> 30 days) timeouts in a special Memcached deals with long (> 30 days) timeouts in a special
way. Call this function to obtain a safe value for your timeout. way. Call this function to obtain a safe value for your timeout.
""" """
timeout = timeout or self.default_timeout if timeout == DEFAULT_TIMEOUT:
return self.default_timeout
if timeout is None:
# Using 0 in memcache sets a non-expiring timeout.
return 0
elif int(timeout) == 0:
# Other cache backends treat 0 as set-and-expire. To achieve this
# in memcache backends, a negative timeout must be passed.
timeout = -1
if timeout > 2592000: # 60*60*24*30, 30 days if timeout > 2592000: # 60*60*24*30, 30 days
# See http://code.google.com/p/memcached/wiki/FAQ # See http://code.google.com/p/memcached/wiki/FAQ
# "You can set expire times up to 30 days in the future. After that # "You can set expire times up to 30 days in the future. After that
@ -56,7 +66,7 @@ class BaseMemcachedCache(BaseCache):
# Python 2 memcache requires the key to be a byte string. # Python 2 memcache requires the key to be a byte string.
return force_str(super(BaseMemcachedCache, self).make_key(key, version)) return force_str(super(BaseMemcachedCache, self).make_key(key, version))
def add(self, key, value, timeout=0, version=None): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) 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_memcache_timeout(timeout))
@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache):
return default return default
return val return val
def set(self, key, value, timeout=0, version=None): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self._cache.set(key, value, self._get_memcache_timeout(timeout)) self._cache.set(key, value, self._get_memcache_timeout(timeout))
@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache):
raise ValueError("Key '%s' not found" % key) raise ValueError("Key '%s' not found" % key)
return val return val
def set_many(self, data, timeout=0, version=None): def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
safe_data = {} safe_data = {}
for key, value in data.items(): for key, value in data.items():
key = self.make_key(key, version=version) key = self.make_key(key, version=version)

View File

@ -485,6 +485,12 @@ Miscellaneous
changes in 1.6 particularly affect :class:`~django.forms.DecimalField` and changes in 1.6 particularly affect :class:`~django.forms.DecimalField` and
:class:`~django.forms.ModelMultipleChoiceField`. :class:`~django.forms.ModelMultipleChoiceField`.
* There have been changes in the way timeouts are handled in cache backends.
Explicitly passing in ``timeout=None`` no longer results in using the
default timeout. It will now set a non-expiring timeout. Passing 0 into the
memcache backend no longer uses the default timeout, and now will
set-and-expire-immediately the value.
Features deprecated in 1.6 Features deprecated in 1.6
========================== ==========================

View File

@ -707,10 +707,15 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``::
>>> cache.get('my_key') >>> cache.get('my_key')
'hello, world!' 'hello, world!'
The ``timeout`` argument is optional and defaults to the ``timeout`` The ``timeout`` argument is optional and defaults to the ``timeout`` argument
argument of the appropriate backend in the :setting:`CACHES` setting of the appropriate backend in the :setting:`CACHES` setting (explained above).
(explained above). It's the number of seconds the value should be stored It's the number of seconds the value should be stored in the cache. Passing in
in the cache. ``None`` for ``timeout`` will cache the value forever.
.. versionchanged:: 1.6
Previously, passing ``None`` explicitly would use the default timeout
value.
If the object doesn't exist in the cache, ``cache.get()`` returns ``None``:: If the object doesn't exist in the cache, ``cache.get()`` returns ``None``::

28
tests/cache/tests.py vendored
View File

@ -441,6 +441,34 @@ class BaseCacheTests(object):
self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key3'), 'sausage')
self.assertEqual(self.cache.get('key4'), 'lobster bisque') self.assertEqual(self.cache.get('key4'), 'lobster bisque')
def test_forever_timeout(self):
'''
Passing in None into timeout results in a value that is cached forever
'''
self.cache.set('key1', 'eggs', None)
self.assertEqual(self.cache.get('key1'), 'eggs')
self.cache.add('key2', 'ham', None)
self.assertEqual(self.cache.get('key2'), 'ham')
self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None)
self.assertEqual(self.cache.get('key3'), 'sausage')
self.assertEqual(self.cache.get('key4'), 'lobster bisque')
def test_zero_timeout(self):
'''
Passing in None into timeout results in a value that is cached forever
'''
self.cache.set('key1', 'eggs', 0)
self.assertEqual(self.cache.get('key1'), None)
self.cache.add('key2', 'ham', 0)
self.assertEqual(self.cache.get('key2'), None)
self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0)
self.assertEqual(self.cache.get('key3'), None)
self.assertEqual(self.cache.get('key4'), None)
def test_float_timeout(self): def test_float_timeout(self):
# Make sure a timeout given as a float doesn't crash anything. # Make sure a timeout given as a float doesn't crash anything.
self.cache.set("key1", "spam", 100.2) self.cache.set("key1", "spam", 100.2)