Browse Source

Fixed #29887 -- Added a cache backend for pymemcache.

Nick Pope 6 years ago
parent
commit
b4d46df5ca

+ 14 - 0
django/core/cache/backends/memcached.py

@@ -214,3 +214,17 @@ class PyLibMCCache(BaseMemcachedCache):
         # libmemcached manages its own connections. Don't call disconnect_all()
         # as it resets the failover state and creates unnecessary reconnects.
         pass
+
+
+class PyMemcacheCache(BaseMemcachedCache):
+    """An implementation of a cache binding using pymemcache."""
+    def __init__(self, server, params):
+        import pymemcache.serde
+        super().__init__(server, params, library=pymemcache, value_not_found_exception=KeyError)
+        self._class = self._lib.HashClient
+        self._options = {
+            'allow_unicode_keys': True,
+            'default_noreply': False,
+            'serde': pymemcache.serde.pickle_serde,
+            **self._options,
+        }

+ 5 - 0
docs/ref/settings.txt

@@ -158,11 +158,16 @@ The cache backend to use. The built-in cache backends are:
 * ``'django.core.cache.backends.locmem.LocMemCache'``
 * ``'django.core.cache.backends.memcached.MemcachedCache'``
 * ``'django.core.cache.backends.memcached.PyLibMCCache'``
+* ``'django.core.cache.backends.memcached.PyMemcacheCache'``
 
 You can use a cache backend that doesn't ship with Django by setting
 :setting:`BACKEND <CACHES-BACKEND>` to a fully-qualified path of a cache
 backend class (i.e. ``mypackage.backends.whatever.WhateverCache``).
 
+.. versionchanged:: 3.2
+
+    The ``PyMemcacheCache`` backend was added.
+
 .. setting:: CACHES-KEY_FUNCTION
 
 ``KEY_FUNCTION``

+ 10 - 0
docs/releases/3.2.txt

@@ -53,6 +53,16 @@ needed. As a consequence, it's deprecated.
 
 See :ref:`configuring-applications-ref` for full details.
 
+``pymemcache`` support
+----------------------
+
+The new ``django.core.cache.backends.memcached.PyMemcacheCache`` cache backend
+allows using the pymemcache_ library for memcached. ``pymemcache`` 3.4.0 or
+higher is required. For more details, see the :doc:`documentation on caching in
+Django </topics/cache>`.
+
+.. _pymemcache: https://pypi.org/project/pymemcache/
+
 Minor features
 --------------
 

+ 28 - 4
docs/topics/cache.txt

@@ -77,17 +77,19 @@ database or filesystem usage.
 
 After installing Memcached itself, you'll need to install a Memcached
 binding. There are several Python Memcached bindings available; the
-two most common are `python-memcached`_ and `pylibmc`_.
+three most common are `python-memcached`_, `pylibmc`_, and `pymemcache`_.
 
 .. _`python-memcached`: https://pypi.org/project/python-memcached/
 .. _`pylibmc`: https://pypi.org/project/pylibmc/
+.. _`pymemcache`: https://pypi.org/project/pymemcache/
 
 To use Memcached with Django:
 
 * Set :setting:`BACKEND <CACHES-BACKEND>` to
-  ``django.core.cache.backends.memcached.MemcachedCache`` or
-  ``django.core.cache.backends.memcached.PyLibMCCache`` (depending
-  on your chosen memcached binding)
+  ``django.core.cache.backends.memcached.MemcachedCache``,
+  ``django.core.cache.backends.memcached.PyLibMCCache``, or
+  ``django.core.cache.backends.memcached.PyMemcacheCache`` (depending on your
+  chosen memcached binding)
 
 * Set :setting:`LOCATION <CACHES-LOCATION>` to ``ip:port`` values,
   where ``ip`` is the IP address of the Memcached daemon and ``port`` is the
@@ -159,6 +161,10 @@ permanent storage -- they're all intended to be solutions for caching, not
 storage -- but we point this out here because memory-based caching is
 particularly temporary.
 
+.. versionchanged:: 3.2
+
+    The ``PyMemcacheCache`` backend was added.
+
 .. _database-caching:
 
 Database caching
@@ -466,6 +472,24 @@ the binary protocol, SASL authentication, and the ``ketama`` behavior mode::
         }
     }
 
+Here's an example configuration for a ``pymemcache`` based backend that enables
+client pooling (which may improve performance by keeping clients connected),
+treats memcache/network errors as cache misses, and sets the ``TCP_NODELAY``
+flag on the connection's socket::
+
+    CACHES = {
+        'default': {
+            'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
+            'LOCATION': '127.0.0.1:11211',
+            'OPTIONS': {
+                'no_delay': True,
+                'ignore_exc': True,
+                'max_pool_size': 4,
+                'use_pooling': True,
+            }
+        }
+    }
+
 .. _the-per-site-cache:
 
 The per-site cache

+ 31 - 0
tests/cache/tests.py

@@ -1277,6 +1277,7 @@ for _cache_params in settings.CACHES.values():
 
 MemcachedCache_params = configured_caches.get('django.core.cache.backends.memcached.MemcachedCache')
 PyLibMCCache_params = configured_caches.get('django.core.cache.backends.memcached.PyLibMCCache')
+PyMemcacheCache_params = configured_caches.get('django.core.cache.backends.memcached.PyMemcacheCache')
 
 # The memcached backends don't support cull-related options like `MAX_ENTRIES`.
 memcached_excluded_caches = {'cull', 'zero_cull'}
@@ -1459,6 +1460,36 @@ class PyLibMCCacheTests(BaseMemcachedTests, TestCase):
                 self.assertEqual(cache.client_servers, [expected])
 
 
+@unittest.skipUnless(PyMemcacheCache_params, 'PyMemcacheCache backend not configured')
+@override_settings(CACHES=caches_setting_for_tests(
+    base=PyMemcacheCache_params,
+    exclude=memcached_excluded_caches,
+))
+class PyMemcacheCacheTests(BaseMemcachedTests, TestCase):
+    base_params = PyMemcacheCache_params
+
+    def test_pymemcache_highest_pickle_version(self):
+        self.assertEqual(
+            cache._cache.default_kwargs['serde']._serialize_func.keywords['pickle_version'],
+            pickle.HIGHEST_PROTOCOL,
+        )
+        for cache_key in settings.CACHES:
+            for client_key, client in caches[cache_key]._cache.clients.items():
+                with self.subTest(cache_key=cache_key, server=client_key):
+                    self.assertEqual(
+                        client.serde._serialize_func.keywords['pickle_version'],
+                        pickle.HIGHEST_PROTOCOL,
+                    )
+
+    @override_settings(CACHES=caches_setting_for_tests(
+        base=PyMemcacheCache_params,
+        exclude=memcached_excluded_caches,
+        OPTIONS={'no_delay': True},
+    ))
+    def test_pymemcache_options(self):
+        self.assertIs(cache._cache.default_kwargs['no_delay'], True)
+
+
 @override_settings(CACHES=caches_setting_for_tests(
     BACKEND='django.core.cache.backends.filebased.FileBasedCache',
 ))

+ 1 - 0
tests/requirements/py3.txt

@@ -8,6 +8,7 @@ numpy
 Pillow >= 6.2.0
 # pylibmc/libmemcached can't be built on Windows.
 pylibmc; sys.platform != 'win32'
+pymemcache >= 3.4.0
 python-memcached >= 1.59
 pytz
 pywatchman; sys.platform != 'win32'