diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index ca45fc99fa..e254527cea 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -1,5 +1,7 @@ "Memcached cache backend" +import time + from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError from django.utils.encoding import smart_unicode, smart_str @@ -16,10 +18,26 @@ class CacheClass(BaseCache): BaseCache.__init__(self, params) self._cache = memcache.Client(server.split(';')) + def _get_memcache_timeout(self, timeout): + """ + Memcached deals with long (> 30 days) timeouts in a special + way. Call this function to obtain a safe value for your timeout. + """ + timeout = timeout or self.default_timeout + if timeout > 2592000: # 60*60*24*30, 30 days + # See http://code.google.com/p/memcached/wiki/FAQ + # "You can set expire times up to 30 days in the future. After that + # memcached interprets it as a date, and will expire the item after + # said date. This is a simple (but obscure) mechanic." + # + # This means that we have to switch to absolute timestamps. + timeout += int(time.time()) + return timeout + def add(self, key, value, timeout=0): if isinstance(value, unicode): value = value.encode('utf-8') - return self._cache.add(smart_str(key), value, timeout or self.default_timeout) + return self._cache.add(smart_str(key), value, self._get_memcache_timeout(timeout)) def get(self, key, default=None): val = self._cache.get(smart_str(key)) @@ -34,7 +52,7 @@ class CacheClass(BaseCache): def set(self, key, value, timeout=0): if isinstance(value, unicode): value = value.encode('utf-8') - self._cache.set(smart_str(key), value, timeout or self.default_timeout) + self._cache.set(smart_str(key), value, self._get_memcache_timeout(timeout)) def delete(self, key): self._cache.delete(smart_str(key)) @@ -78,7 +96,7 @@ class CacheClass(BaseCache): if isinstance(value, unicode): value = value.encode('utf-8') safe_data[smart_str(key)] = value - self._cache.set_multi(safe_data, timeout or self.default_timeout) + self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout)) def delete_many(self, keys): self._cache.delete_multi(map(smart_str, keys)) diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 86d8ca27e0..6ca4d8bce8 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -324,6 +324,22 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get("key1"), None) self.assertEqual(self.cache.get("key2"), None) + def test_long_timeout(self): + ''' + Using a timeout greater than 30 days makes memcached think + it is an absolute expiration timestamp instead of a relative + offset. Test that we honour this convention. Refs #12399. + ''' + self.cache.set('key1', 'eggs', 60*60*24*30 + 1) #30 days + 1 second + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', 60*60*24*30 + 1) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 60*60*24*30 + 1) + self.assertEqual(self.cache.get('key3'), 'sausage') + self.assertEqual(self.cache.get('key4'), 'lobster bisque') + class DBCacheTests(unittest.TestCase, BaseCacheTests): def setUp(self): management.call_command('createcachetable', 'test_cache_table', verbosity=0, interactive=False)