Fixed #27480 -- Added cache.touch().
This commit is contained in:
parent
8e960c5aba
commit
3246ad1065
|
@ -124,6 +124,13 @@ class BaseCache:
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('subclasses of BaseCache must provide a set() method')
|
raise NotImplementedError('subclasses of BaseCache must provide a set() method')
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
"""
|
||||||
|
Update the key's expiry time using timeout. Return True if successful
|
||||||
|
or False if the key does not exist.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('subclasses of BaseCache must provide a touch() method')
|
||||||
|
|
||||||
def delete(self, key, version=None):
|
def delete(self, key, version=None):
|
||||||
"""
|
"""
|
||||||
Delete a key from the cache, failing silently.
|
Delete a key from the cache, failing silently.
|
||||||
|
|
|
@ -104,6 +104,11 @@ class DatabaseCache(BaseDatabaseCache):
|
||||||
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 touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
key = self.make_key(key, version=version)
|
||||||
|
self.validate_key(key)
|
||||||
|
return self._base_set('touch', key, None, timeout)
|
||||||
|
|
||||||
def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT):
|
def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT):
|
||||||
timeout = self.get_backend_timeout(timeout)
|
timeout = self.get_backend_timeout(timeout)
|
||||||
db = router.db_for_write(self.cache_model_class)
|
db = router.db_for_write(self.cache_model_class)
|
||||||
|
@ -157,7 +162,16 @@ class DatabaseCache(BaseDatabaseCache):
|
||||||
current_expires = converter(current_expires, expression, connection)
|
current_expires = converter(current_expires, expression, connection)
|
||||||
|
|
||||||
exp = connection.ops.adapt_datetimefield_value(exp)
|
exp = connection.ops.adapt_datetimefield_value(exp)
|
||||||
if result and (mode == 'set' or (mode == 'add' and current_expires < now)):
|
if result and mode == 'touch':
|
||||||
|
cursor.execute(
|
||||||
|
'UPDATE %s SET %s = %%s WHERE %s = %%s' % (
|
||||||
|
table,
|
||||||
|
quote_name('expires'),
|
||||||
|
quote_name('cache_key')
|
||||||
|
),
|
||||||
|
[exp, key]
|
||||||
|
)
|
||||||
|
elif result and (mode == 'set' or (mode == 'add' and current_expires < now)):
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'UPDATE %s SET %s = %%s, %s = %%s WHERE %s = %%s' % (
|
'UPDATE %s SET %s = %%s, %s = %%s WHERE %s = %%s' % (
|
||||||
table,
|
table,
|
||||||
|
@ -167,7 +181,7 @@ class DatabaseCache(BaseDatabaseCache):
|
||||||
),
|
),
|
||||||
[b64encoded, exp, key]
|
[b64encoded, exp, key]
|
||||||
)
|
)
|
||||||
else:
|
elif mode != 'touch':
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'INSERT INTO %s (%s, %s, %s) VALUES (%%s, %%s, %%s)' % (
|
'INSERT INTO %s (%s, %s, %s) VALUES (%%s, %%s, %%s)' % (
|
||||||
table,
|
table,
|
||||||
|
@ -177,6 +191,8 @@ class DatabaseCache(BaseDatabaseCache):
|
||||||
),
|
),
|
||||||
[key, b64encoded, exp]
|
[key, b64encoded, exp]
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return False # touch failed.
|
||||||
except DatabaseError:
|
except DatabaseError:
|
||||||
# To be threadsafe, updates/inserts are allowed to fail silently
|
# To be threadsafe, updates/inserts are allowed to fail silently
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -21,6 +21,10 @@ class DummyCache(BaseCache):
|
||||||
key = self.make_key(key, version=version)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
self.validate_key(key)
|
||||||
|
return False
|
||||||
|
|
||||||
def delete(self, key, version=None):
|
def delete(self, key, version=None):
|
||||||
key = self.make_key(key, version=version)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
|
|
|
@ -9,9 +9,15 @@ import time
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
|
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
|
||||||
|
from django.core.files import locks
|
||||||
from django.core.files.move import file_move_safe
|
from django.core.files.move import file_move_safe
|
||||||
|
|
||||||
|
|
||||||
|
def _write_content(f, expiry, value):
|
||||||
|
f.write(pickle.dumps(expiry, pickle.HIGHEST_PROTOCOL))
|
||||||
|
f.write(zlib.compress(pickle.dumps(value, pickle.HIGHEST_PROTOCOL)))
|
||||||
|
|
||||||
|
|
||||||
class FileBasedCache(BaseCache):
|
class FileBasedCache(BaseCache):
|
||||||
cache_suffix = '.djcache'
|
cache_suffix = '.djcache'
|
||||||
|
|
||||||
|
@ -45,14 +51,30 @@ class FileBasedCache(BaseCache):
|
||||||
try:
|
try:
|
||||||
with open(fd, 'wb') as f:
|
with open(fd, 'wb') as f:
|
||||||
expiry = self.get_backend_timeout(timeout)
|
expiry = self.get_backend_timeout(timeout)
|
||||||
f.write(pickle.dumps(expiry, pickle.HIGHEST_PROTOCOL))
|
_write_content(f, expiry, value)
|
||||||
f.write(zlib.compress(pickle.dumps(value, pickle.HIGHEST_PROTOCOL)))
|
|
||||||
file_move_safe(tmp_path, fname, allow_overwrite=True)
|
file_move_safe(tmp_path, fname, allow_overwrite=True)
|
||||||
renamed = True
|
renamed = True
|
||||||
finally:
|
finally:
|
||||||
if not renamed:
|
if not renamed:
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
try:
|
||||||
|
with open(self._key_to_file(key, version), 'r+b') as f:
|
||||||
|
try:
|
||||||
|
locks.lock(f, locks.LOCK_EX)
|
||||||
|
if self._is_expired(f):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
previous_value = pickle.loads(zlib.decompress(f.read()))
|
||||||
|
f.seek(0)
|
||||||
|
_write_content(f, self.get_backend_timeout(timeout), previous_value)
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
locks.unlock(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
def delete(self, key, version=None):
|
def delete(self, key, version=None):
|
||||||
self._delete(self._key_to_file(key, version))
|
self._delete(self._key_to_file(key, version))
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,14 @@ class LocMemCache(BaseCache):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._set(key, pickled, timeout)
|
self._set(key, pickled, timeout)
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
key = self.make_key(key, version=version)
|
||||||
|
with self._lock:
|
||||||
|
if self._has_expired(key):
|
||||||
|
return False
|
||||||
|
self._expire_info[key] = self.get_backend_timeout(timeout)
|
||||||
|
return True
|
||||||
|
|
||||||
def incr(self, key, delta=1, version=None):
|
def incr(self, key, delta=1, version=None):
|
||||||
key = self.make_key(key, version=version)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
|
|
|
@ -162,6 +162,10 @@ class MemcachedCache(BaseMemcachedCache):
|
||||||
self._client = self._lib.Client(self._servers, **client_kwargs)
|
self._client = self._lib.Client(self._servers, **client_kwargs)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
key = self.make_key(key, version=version)
|
||||||
|
return self._cache.touch(key, self.get_backend_timeout(timeout)) != 0
|
||||||
|
|
||||||
|
|
||||||
class PyLibMCCache(BaseMemcachedCache):
|
class PyLibMCCache(BaseMemcachedCache):
|
||||||
"An implementation of a cache binding using pylibmc"
|
"An implementation of a cache binding using pylibmc"
|
||||||
|
@ -173,6 +177,12 @@ class PyLibMCCache(BaseMemcachedCache):
|
||||||
def _cache(self):
|
def _cache(self):
|
||||||
return self._lib.Client(self._servers, **self._options)
|
return self._lib.Client(self._servers, **self._options)
|
||||||
|
|
||||||
|
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
|
key = self.make_key(key, version=version)
|
||||||
|
if timeout == 0:
|
||||||
|
return self._cache.delete(key)
|
||||||
|
return self._cache.touch(key, self.get_backend_timeout(timeout))
|
||||||
|
|
||||||
def close(self, **kwargs):
|
def close(self, **kwargs):
|
||||||
# libmemcached manages its own connections. Don't call disconnect_all()
|
# libmemcached manages its own connections. Don't call disconnect_all()
|
||||||
# as it resets the failover state and creates unnecessary reconnects.
|
# as it resets the failover state and creates unnecessary reconnects.
|
||||||
|
|
|
@ -141,6 +141,9 @@ Cache
|
||||||
* The :ref:`local-memory cache backend <local-memory-caching>` now uses a
|
* The :ref:`local-memory cache backend <local-memory-caching>` now uses a
|
||||||
least-recently-used (LRU) culling strategy rather than a pseudo-random one.
|
least-recently-used (LRU) culling strategy rather than a pseudo-random one.
|
||||||
|
|
||||||
|
* The new ``touch()`` method of the :ref:`low-level cache API
|
||||||
|
<low-level-cache-api>` updates the timeout of cache keys.
|
||||||
|
|
||||||
CSRF
|
CSRF
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
|
|
|
@ -734,6 +734,7 @@ a cached item, for example:
|
||||||
>>> key = make_template_fragment_key('sidebar', [username])
|
>>> key = make_template_fragment_key('sidebar', [username])
|
||||||
>>> cache.delete(key) # invalidates cached template fragment
|
>>> cache.delete(key) # invalidates cached template fragment
|
||||||
|
|
||||||
|
.. _low-level-cache-api:
|
||||||
|
|
||||||
The low-level cache API
|
The low-level cache API
|
||||||
=======================
|
=======================
|
||||||
|
@ -891,6 +892,22 @@ from the cache, not just the keys set by your application. ::
|
||||||
|
|
||||||
>>> cache.clear()
|
>>> cache.clear()
|
||||||
|
|
||||||
|
``cache.touch()`` sets a new expiration for a key. For example, to update a key
|
||||||
|
to expire 10 seconds from now::
|
||||||
|
|
||||||
|
>>> cache.touch('a', 10)
|
||||||
|
True
|
||||||
|
|
||||||
|
Like other methods, the ``timeout`` argument is optional and defaults to the
|
||||||
|
``TIMEOUT`` option of the appropriate backend in the :setting:`CACHES` setting.
|
||||||
|
|
||||||
|
``touch()`` returns ``True`` if the key was successfully touched, ``False``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.1
|
||||||
|
|
||||||
|
The ``cache.touch()`` method was added.
|
||||||
|
|
||||||
You can also increment or decrement a key that already exists using the
|
You can also increment or decrement a key that already exists using the
|
||||||
``incr()`` or ``decr()`` methods, respectively. By default, the existing cache
|
``incr()`` or ``decr()`` methods, respectively. By default, the existing cache
|
||||||
value will be incremented or decremented by 1. Other increment/decrement values
|
value will be incremented or decremented by 1. Other increment/decrement values
|
||||||
|
|
|
@ -141,6 +141,10 @@ class DummyCacheTests(SimpleTestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
cache.decr('does_not_exist')
|
cache.decr('does_not_exist')
|
||||||
|
|
||||||
|
def test_touch(self):
|
||||||
|
"""Dummy cache can't do touch()."""
|
||||||
|
self.assertIs(cache.touch('whatever'), False)
|
||||||
|
|
||||||
def test_data_types(self):
|
def test_data_types(self):
|
||||||
"All data types are ignored equally by the dummy cache"
|
"All data types are ignored equally by the dummy cache"
|
||||||
stuff = {
|
stuff = {
|
||||||
|
@ -427,6 +431,23 @@ class BaseCacheTests:
|
||||||
self.assertEqual(cache.get("expire2"), "newvalue")
|
self.assertEqual(cache.get("expire2"), "newvalue")
|
||||||
self.assertFalse(cache.has_key("expire3"))
|
self.assertFalse(cache.has_key("expire3"))
|
||||||
|
|
||||||
|
def test_touch(self):
|
||||||
|
# cache.touch() updates the timeout.
|
||||||
|
cache.set('expire1', 'very quickly', timeout=1)
|
||||||
|
self.assertIs(cache.touch('expire1', timeout=4), True)
|
||||||
|
time.sleep(2)
|
||||||
|
self.assertTrue(cache.has_key('expire1'))
|
||||||
|
time.sleep(3)
|
||||||
|
self.assertFalse(cache.has_key('expire1'))
|
||||||
|
|
||||||
|
# cache.touch() works without the timeout argument.
|
||||||
|
cache.set('expire1', 'very quickly', timeout=1)
|
||||||
|
self.assertIs(cache.touch('expire1'), True)
|
||||||
|
time.sleep(2)
|
||||||
|
self.assertTrue(cache.has_key('expire1'))
|
||||||
|
|
||||||
|
self.assertIs(cache.touch('nonexistent'), False)
|
||||||
|
|
||||||
def test_unicode(self):
|
def test_unicode(self):
|
||||||
# Unicode values can be cached
|
# Unicode values can be cached
|
||||||
stuff = {
|
stuff = {
|
||||||
|
@ -549,6 +570,11 @@ class BaseCacheTests:
|
||||||
self.assertEqual(cache.get('key3'), 'sausage')
|
self.assertEqual(cache.get('key3'), 'sausage')
|
||||||
self.assertEqual(cache.get('key4'), 'lobster bisque')
|
self.assertEqual(cache.get('key4'), 'lobster bisque')
|
||||||
|
|
||||||
|
cache.set('key5', 'belgian fries', timeout=1)
|
||||||
|
cache.touch('key5', timeout=None)
|
||||||
|
time.sleep(2)
|
||||||
|
self.assertEqual(cache.get('key5'), 'belgian fries')
|
||||||
|
|
||||||
def test_zero_timeout(self):
|
def test_zero_timeout(self):
|
||||||
"""
|
"""
|
||||||
Passing in zero into timeout results in a value that is not cached
|
Passing in zero into timeout results in a value that is not cached
|
||||||
|
@ -563,6 +589,10 @@ class BaseCacheTests:
|
||||||
self.assertIsNone(cache.get('key3'))
|
self.assertIsNone(cache.get('key3'))
|
||||||
self.assertIsNone(cache.get('key4'))
|
self.assertIsNone(cache.get('key4'))
|
||||||
|
|
||||||
|
cache.set('key5', 'belgian fries', timeout=5)
|
||||||
|
cache.touch('key5', timeout=0)
|
||||||
|
self.assertIsNone(cache.get('key5'))
|
||||||
|
|
||||||
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.
|
||||||
cache.set("key1", "spam", 100.2)
|
cache.set("key1", "spam", 100.2)
|
||||||
|
|
Loading…
Reference in New Issue