Kaydet (Commit) 89955cc3 authored tarafından Jacob Burch's avatar Jacob Burch Kaydeden (comit) Aymeric Augustin

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

Also, streamline the use of 0 and None between cache backends.
üst e0df6471
...@@ -123,6 +123,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -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>
......
...@@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -9,7 +9,7 @@ except ImportError: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
"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): ...@@ -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): ...@@ -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):
......
...@@ -9,9 +9,10 @@ try: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -6,7 +6,7 @@ try: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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():
......
...@@ -4,7 +4,7 @@ import time ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -485,6 +485,12 @@ Miscellaneous ...@@ -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
========================== ==========================
......
...@@ -707,10 +707,15 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: ...@@ -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``::
......
...@@ -441,6 +441,34 @@ class BaseCacheTests(object): ...@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment