Nicolas Noé 7 лет назад
Родитель
Сommit
3246ad1065

+ 7 - 0
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.

+ 18 - 2
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

+ 4 - 0
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)

+ 24 - 2
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))
 

+ 8 - 0
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)

+ 10 - 0
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.

+ 3 - 0
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
 ~~~~
 

+ 17 - 0
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

+ 30 - 0
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)