From a027447f5606d60358e5da76c0e8233732ad2b17 Mon Sep 17 00:00:00 2001 From: Olivier Tabone Date: Sun, 6 Nov 2016 12:57:38 +0100 Subject: [PATCH] Fixed #27318 -- Made cache.set_many() return the list of failed keys. --- AUTHORS | 1 + django/core/cache/backends/base.py | 4 ++++ django/core/cache/backends/memcached.py | 13 ++++++++----- docs/releases/2.0.txt | 3 ++- docs/topics/cache.txt | 7 +++++++ tests/cache/tests.py | 14 ++++++++++++++ 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4b832bfdb4..899190a167 100644 --- a/AUTHORS +++ b/AUTHORS @@ -609,6 +609,7 @@ answer newbie questions, and generally made Django that much better: Oliver Beattie Oliver Rutherfurd Olivier Sels + Olivier Tabone Orestis Markou Orne Brocaar Oscar Ramirez diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index a0ed8275f9..aaf34c042e 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -206,9 +206,13 @@ class BaseCache: If timeout is given, use that timeout for the key; otherwise use the default cache timeout. + + On backends that support it, return a list of keys that failed + insertion, or an empty list if all keys were inserted successfully. """ for key, value in data.items(): self.set(key, value, timeout=timeout, version=version) + return [] def delete_many(self, keys, version=None): """ diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index 5af28d7ac3..d49fd148fd 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -134,11 +134,14 @@ class BaseMemcachedCache(BaseCache): return val def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): - safe_data = { - self.make_key(key, version=version): value - for key, value in data.items() - } - self._cache.set_multi(safe_data, self.get_backend_timeout(timeout)) + safe_data = {} + original_keys = {} + for key, value in data.items(): + safe_key = self.make_key(key, version=version) + safe_data[safe_key] = value + original_keys[safe_key] = key + failed_keys = self._cache.set_multi(safe_data, self.get_backend_timeout(timeout)) + return [original_keys[k] for k in failed_keys] def delete_many(self, keys, version=None): self._cache.delete_multi(self.make_key(key, version=version) for key in keys) diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index e50bd36d93..c8ec169e3c 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -157,7 +157,8 @@ Minor features Cache ~~~~~ -* ... +* On memcached, ``cache.set_many()`` returns a list of keys that failed to be + inserted. CSRF ~~~~ diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 7d32f1f94f..22230d6684 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -881,6 +881,13 @@ of key-value pairs:: Like ``cache.set()``, ``set_many()`` takes an optional ``timeout`` parameter. +On supported backends (memcached), ``set_many()`` returns a list of keys that +failed to be inserted. + +.. versionchanged:: 2.0 + + The return value containing list of failing keys was added. + You can delete keys explicitly with ``delete()``. This is an easy way of clearing the cache for a particular object:: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index cef000cb02..e093dc842d 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -470,6 +470,11 @@ class BaseCacheTests: self.assertEqual(cache.get("key1"), "spam") self.assertEqual(cache.get("key2"), "eggs") + def test_set_many_returns_empty_list_on_success(self): + """set_many() returns an empty list when all keys are inserted.""" + failing_keys = cache.set_many({'key1': 'spam', 'key2': 'eggs'}) + self.assertEqual(failing_keys, []) + def test_set_many_expiration(self): # set_many takes a second ``timeout`` parameter cache.set_many({"key1": "spam", "key2": "eggs"}, 1) @@ -1239,6 +1244,13 @@ class BaseMemcachedTests(BaseCacheTests): finally: signals.request_finished.connect(close_old_connections) + def test_set_many_returns_failing_keys(self): + def fail_set_multi(mapping, *args, **kwargs): + return mapping.keys() + with mock.patch('%s.Client.set_multi' % self.client_library_name, side_effect=fail_set_multi): + failing_keys = cache.set_many({'key': 'value'}) + self.assertEqual(failing_keys, ['key']) + @unittest.skipUnless(MemcachedCache_params, "MemcachedCache backend not configured") @override_settings(CACHES=caches_setting_for_tests( @@ -1247,6 +1259,7 @@ class BaseMemcachedTests(BaseCacheTests): )) class MemcachedCacheTests(BaseMemcachedTests, TestCase): base_params = MemcachedCache_params + client_library_name = 'memcache' def test_memcached_uses_highest_pickle_version(self): # Regression test for #19810 @@ -1270,6 +1283,7 @@ class MemcachedCacheTests(BaseMemcachedTests, TestCase): )) class PyLibMCCacheTests(BaseMemcachedTests, TestCase): base_params = PyLibMCCache_params + client_library_name = 'pylibmc' # libmemcached manages its own connections. should_disconnect_on_close = False