summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas NoƩ <nicolas@niconoe.org>2018-04-27 23:48:35 +0200
committerTim Graham <timograham@gmail.com>2018-04-27 17:48:35 -0400
commit3246ad106517e61437f80e8ef3c9d216754039e7 (patch)
tree29fba867b071b61c9cfcc1d33d4662c91f1e40fa
parent8e960c5aba70d834f54b0bc66f2fb5e922a35b41 (diff)
Fixed #27480 -- Added cache.touch().
-rw-r--r--django/core/cache/backends/base.py7
-rw-r--r--django/core/cache/backends/db.py20
-rw-r--r--django/core/cache/backends/dummy.py4
-rw-r--r--django/core/cache/backends/filebased.py26
-rw-r--r--django/core/cache/backends/locmem.py8
-rw-r--r--django/core/cache/backends/memcached.py10
-rw-r--r--docs/releases/2.1.txt3
-rw-r--r--docs/topics/cache.txt17
-rw-r--r--tests/cache/tests.py30
9 files changed, 121 insertions, 4 deletions
diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py
index cf0df7cc68..0d0b1ddc82 100644
--- a/django/core/cache/backends/base.py
+++ b/django/core/cache/backends/base.py
@@ -124,6 +124,13 @@ class BaseCache:
"""
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):
"""
Delete a key from the cache, failing silently.
diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py
index cc92284709..d782f2191d 100644
--- a/django/core/cache/backends/db.py
+++ b/django/core/cache/backends/db.py
@@ -104,6 +104,11 @@ class DatabaseCache(BaseDatabaseCache):
self.validate_key(key)
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):
timeout = self.get_backend_timeout(timeout)
db = router.db_for_write(self.cache_model_class)
@@ -157,7 +162,16 @@ class DatabaseCache(BaseDatabaseCache):
current_expires = converter(current_expires, expression, connection)
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(
'UPDATE %s SET %s = %%s, %s = %%s WHERE %s = %%s' % (
table,
@@ -167,7 +181,7 @@ class DatabaseCache(BaseDatabaseCache):
),
[b64encoded, exp, key]
)
- else:
+ elif mode != 'touch':
cursor.execute(
'INSERT INTO %s (%s, %s, %s) VALUES (%%s, %%s, %%s)' % (
table,
@@ -177,6 +191,8 @@ class DatabaseCache(BaseDatabaseCache):
),
[key, b64encoded, exp]
)
+ else:
+ return False # touch failed.
except DatabaseError:
# To be threadsafe, updates/inserts are allowed to fail silently
return False
diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py
index 4de2485be6..942b72ef89 100644
--- a/django/core/cache/backends/dummy.py
+++ b/django/core/cache/backends/dummy.py
@@ -21,6 +21,10 @@ class DummyCache(BaseCache):
key = self.make_key(key, version=version)
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):
key = self.make_key(key, version=version)
self.validate_key(key)
diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py
index e4a363dcd7..287d6472a5 100644
--- a/django/core/cache/backends/filebased.py
+++ b/django/core/cache/backends/filebased.py
@@ -9,9 +9,15 @@ import time
import zlib
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
+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):
cache_suffix = '.djcache'
@@ -45,14 +51,30 @@ class FileBasedCache(BaseCache):
try:
with open(fd, 'wb') as f:
expiry = self.get_backend_timeout(timeout)
- f.write(pickle.dumps(expiry, pickle.HIGHEST_PROTOCOL))
- f.write(zlib.compress(pickle.dumps(value, pickle.HIGHEST_PROTOCOL)))
+ _write_content(f, expiry, value)
file_move_safe(tmp_path, fname, allow_overwrite=True)
renamed = True
finally:
if not renamed:
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):
self._delete(self._key_to_file(key, version))
diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py
index cd610bbb73..093144f04a 100644
--- a/django/core/cache/backends/locmem.py
+++ b/django/core/cache/backends/locmem.py
@@ -55,6 +55,14 @@ class LocMemCache(BaseCache):
with self._lock:
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):
key = self.make_key(key, version=version)
self.validate_key(key)
diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py
index faaa36306f..1c8066ba03 100644
--- a/django/core/cache/backends/memcached.py
+++ b/django/core/cache/backends/memcached.py
@@ -162,6 +162,10 @@ class MemcachedCache(BaseMemcachedCache):
self._client = self._lib.Client(self._servers, **client_kwargs)
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):
"An implementation of a cache binding using pylibmc"
@@ -173,6 +177,12 @@ class PyLibMCCache(BaseMemcachedCache):
def _cache(self):
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):
# libmemcached manages its own connections. Don't call disconnect_all()
# as it resets the failover state and creates unnecessary reconnects.
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 7595be46c0..e6b8b29a64 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -141,6 +141,9 @@ Cache
* The :ref:`local-memory cache backend <local-memory-caching>` now uses a
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
~~~~
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index 8c5630f3e2..3ca3a08a4a 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -734,6 +734,7 @@ a cached item, for example:
>>> key = make_template_fragment_key('sidebar', [username])
>>> cache.delete(key) # invalidates cached template fragment
+.. _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.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
``incr()`` or ``decr()`` methods, respectively. By default, the existing cache
value will be incremented or decremented by 1. Other increment/decrement values
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index 95fa5d68e6..890f8b6a9e 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -141,6 +141,10 @@ class DummyCacheTests(SimpleTestCase):
with self.assertRaises(ValueError):
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):
"All data types are ignored equally by the dummy cache"
stuff = {
@@ -427,6 +431,23 @@ class BaseCacheTests:
self.assertEqual(cache.get("expire2"), "newvalue")
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):
# Unicode values can be cached
stuff = {
@@ -549,6 +570,11 @@ class BaseCacheTests:
self.assertEqual(cache.get('key3'), 'sausage')
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):
"""
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('key4'))
+ cache.set('key5', 'belgian fries', timeout=5)
+ cache.touch('key5', timeout=0)
+ self.assertIsNone(cache.get('key5'))
+
def test_float_timeout(self):
# Make sure a timeout given as a float doesn't crash anything.
cache.set("key1", "spam", 100.2)