Skip to content

Commit 3639099

Browse files
4383zzzeek
authored andcommitted
Allow using pymemcache's socket keepalive and retry mechanisms
Socket keepalive capabilities have been introduced [1][2][3] with pymemcache 3.5.0 [4][5]. These changes allow to pass keepalive configuration to the pymemcache client. Retry mechanisms have been implemented [6][7] with pymemcache 3.5.0 [5][8]. These changes allow to instantiate a ``RetryingClient`` by using dogpile.cache. [1] pinterest/pymemcache@b289c87 [2] pinterest/pymemcache@c782de1 [3] pinterest/pymemcache@4d46f5a [4] pinterest/pymemcache@07b5ecc [5] https://pypi.org/project/pymemcache/3.5.0/ [6] pinterest/pymemcache@75fe5c8 [7] https://pymemcache.readthedocs.io/en/latest/getting_started.html#using-the-built-in-retrying-mechanism [8] pinterest/pymemcache@07b5ecc Closes: #205 Pull-request: #205 Pull-request-sha: 8a7ea5d Change-Id: Ia2e76475943bc8ec86b5974217d1c92110be5b43
1 parent 46d8bf1 commit 3639099

6 files changed

Lines changed: 161 additions & 50 deletions

File tree

docs/build/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"sphinx_paramlinks",
4040
]
4141

42-
changelog_sections = ["feature", "bug"]
42+
changelog_sections = ["feature", "usecase", "bug"]
4343

4444
changelog_render_ticket = (
4545
"https://github.com/sqlalchemy/dogpile.cache/issues/%s"

docs/build/unreleased/205.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. change::
2+
:tags: usecase, memcached
3+
4+
Added support for pymemcache socket keepalive and retrying client.
5+
6+
.. seealso::
7+
8+
:paramref:`.PyMemcacheBackend.socket_keepalive`
9+
10+
:paramref:`.PyMemcacheBackend.enable_retry_client`

dogpile/cache/backends/memcached.py

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
import typing
1313
from typing import Any
1414
from typing import Mapping
15+
import warnings
1516

1617
from ..api import CacheBackend
1718
from ..api import NO_VALUE
1819
from ... import util
1920

21+
2022
if typing.TYPE_CHECKING:
2123
import bmemcached
2224
import memcache
@@ -323,16 +325,6 @@ class BMemcachedBackend(GenericMemcachedBackend):
323325
SASL is a standard for adding authentication mechanisms
324326
to protocols in a way that is protocol independent.
325327
326-
SSL/TLS is a security layer on end-to-end communication.
327-
It provides following benefits:
328-
329-
* Encryption: Data is encrypted on the wire between
330-
Memcached client and server.
331-
* Authentication: Optionally, both server and client
332-
authenticate each other.
333-
* Integrity: Data is not tampered or altered when
334-
transmitted between client and server
335-
336328
A typical configuration using username/password::
337329
338330
from dogpile.cache import make_region
@@ -441,27 +433,17 @@ class PyMemcacheBackend(GenericMemcachedBackend):
441433
cache misses.
442434
443435
dogpile.cache uses the ``HashClient`` from pymemcache in order to reduce
444-
API differences when compared to other memcached client drivers. In short,
445-
this allows the user to provide a single server or a list of memcached
436+
API differences when compared to other memcached client drivers.
437+
This allows the user to provide a single server or a list of memcached
446438
servers.
447439
448-
The ``serde`` param defaults to ``pymemcache.serde.pickle_serde`` as the
449-
legacy ``serde`` would always convert the stored data to binary.
450-
451-
The ``default_noreply`` param defaults to False, otherwise the add command
452-
would always return True causing the mutex not to work.
453-
454-
SSL/TLS is a security layer on end-to-end communication.
455-
It provides following benefits:
440+
Arguments which can be passed to the ``arguments``
441+
dictionary include:
456442
457-
* Encryption: Data is encrypted on the wire between
458-
Memcached client and server.
459-
* Authentication: Optionally, both server and client
460-
authenticate each other.
461-
* Integrity: Data is not tampered or altered when
462-
transmitted between client and server
443+
:param tls_context: optional TLS context, will be used for
444+
TLS connections.
463445
464-
A typical configuration using tls_context::
446+
A typical configuration using tls_context::
465447
466448
import ssl
467449
from dogpile.cache import make_region
@@ -477,35 +459,131 @@ class PyMemcacheBackend(GenericMemcachedBackend):
477459
}
478460
)
479461
480-
For advanced ways to configure TLS creating a more complex
481-
tls_context visit https://docs.python.org/3/library/ssl.html
462+
.. seealso::
482463
483-
Arguments which can be passed to the ``arguments``
484-
dictionary include:
464+
`<https://docs.python.org/3/library/ssl.html>`_ - additional TLS
465+
documentation.
485466
486-
:param tls_context: optional TLS context, will be used for
487-
TLS connections.
488467
:param serde: optional "serde". Defaults to
489-
``pymemcache.serde.pickle_serde``
490-
:param default_noreply: Defaults to False
468+
``pymemcache.serde.pickle_serde``.
491469
492-
"""
470+
:param default_noreply: defaults to False. When set to True this flag
471+
enables the pymemcache "noreply" feature. See the pymemcache
472+
documentation for further details.
473+
474+
:param socket_keepalive: optional socket keepalive, will be used for
475+
TCP keepalive configuration. Use of this parameter requires pymemcache
476+
3.5.0 or greater. This parameter
477+
accepts a
478+
`pymemcache.client.base.KeepAliveOpts
479+
<https://pymemcache.readthedocs.io/en/latest/apidoc/pymemcache.client.base.html#pymemcache.client.base.KeepaliveOpts>`_
480+
object.
481+
482+
A typical configuration using ``socket_keepalive``::
483+
484+
from pymemcache import KeepaliveOpts
485+
from dogpile.cache import make_region
486+
487+
# Using the default keepalive configuration
488+
socket_keepalive = KeepaliveOpts()
489+
490+
region = make_region().configure(
491+
'dogpile.cache.pymemcache',
492+
expiration_time = 3600,
493+
arguments = {
494+
'url':["127.0.0.1"],
495+
'socket_keepalive': socket_keepalive
496+
}
497+
)
498+
499+
.. versionadded:: 1.1.4 - added support for ``socket_keepalive``.
500+
501+
:param enable_retry_client: optional flag to enable retry client
502+
mechanisms to handle failure. Defaults to False. When set to ``True``,
503+
the :paramref:`.PyMemcacheBackend.retry_attempts` parameter must also
504+
be set, along with optional parameters
505+
:paramref:`.PyMemcacheBackend.retry_delay`.
506+
:paramref:`.PyMemcacheBackend.retry_for`,
507+
:paramref:`.PyMemcacheBackend.do_not_retry_for`.
508+
509+
.. seealso::
510+
511+
`<https://pymemcache.readthedocs.io/en/latest/getting_started.html#using-the-built-in-retrying-mechanism>`_ -
512+
in the pymemcache documentation
513+
514+
.. versionadded:: 1.1.4
515+
516+
:param retry_attempts: how many times to attempt an action before
517+
failing. Must be 1 or above. Defaults to None.
518+
519+
.. versionadded:: 1.1.4
520+
521+
:param retry_delay: optional int|float, how many seconds to sleep between
522+
each attempt. Defaults to None.
523+
524+
.. versionadded:: 1.1.4
525+
526+
:param retry_for: optional None|tuple|set|list, what exceptions to
527+
allow retries for. Will allow retries for all exceptions if None.
528+
Example: ``(MemcacheClientError, MemcacheUnexpectedCloseError)``
529+
Accepts any class that is a subclass of Exception. Defaults to None.
530+
531+
.. versionadded:: 1.1.4
532+
533+
:param do_not_retry_for: optional None|tuple|set|list, what
534+
exceptions should be retried. Will not block retries for any Exception if
535+
None. Example: ``(IOError, MemcacheIllegalInputError)``
536+
Accepts any class that is a subclass of Exception. Defaults to None.
537+
538+
.. versionadded:: 1.1.4
539+
540+
""" # noqa E501
493541

494542
def __init__(self, arguments):
495543
super().__init__(arguments)
496544

497545
self.serde = arguments.get("serde", pymemcache.serde.pickle_serde)
498546
self.default_noreply = arguments.get("default_noreply", False)
499547
self.tls_context = arguments.get("tls_context", None)
548+
self.socket_keepalive = arguments.get("socket_keepalive", None)
549+
self.enable_retry_client = arguments.get("enable_retry_client", False)
550+
self.retry_attempts = arguments.get("retry_attempts", None)
551+
self.retry_delay = arguments.get("retry_delay", None)
552+
self.retry_for = arguments.get("retry_for", None)
553+
self.do_not_retry_for = arguments.get("do_not_retry_for", None)
554+
if (
555+
self.retry_delay is not None
556+
or self.retry_attempts is not None
557+
or self.retry_for is not None
558+
or self.do_not_retry_for is not None
559+
) and not self.enable_retry_client:
560+
warnings.warn(
561+
"enable_retry_client is not set; retry options "
562+
"will be ignored"
563+
)
500564

501565
def _imports(self):
502566
global pymemcache
503567
import pymemcache
504568

505569
def _create_client(self):
506-
return pymemcache.client.hash.HashClient(
507-
self.url,
508-
serde=self.serde,
509-
default_noreply=self.default_noreply,
510-
tls_context=self.tls_context,
511-
)
570+
_kwargs = {
571+
"serde": self.serde,
572+
"default_noreply": self.default_noreply,
573+
"tls_context": self.tls_context,
574+
}
575+
if self.socket_keepalive is not None:
576+
_kwargs.update({"socket_keepalive": self.socket_keepalive})
577+
578+
client = pymemcache.client.hash.HashClient(self.url, **_kwargs)
579+
580+
if self.enable_retry_client:
581+
return pymemcache.client.retrying.RetryingClient(
582+
client,
583+
attempts=self.retry_attempts,
584+
retry_delay=self.retry_delay,
585+
retry_for=self.retry_for,
586+
do_not_retry_for=self.do_not_retry_for,
587+
)
588+
589+
return client

dogpile/cache/backends/redis.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,7 @@ def get_serialized_multi(self, keys):
173173

174174
def set_serialized(self, key, value):
175175
if self.redis_expiration_time:
176-
self.writer_client.setex(
177-
key,
178-
self.redis_expiration_time,
179-
value,
180-
)
176+
self.writer_client.setex(key, self.redis_expiration_time, value)
181177
else:
182178
self.writer_client.set(key, value)
183179

@@ -283,7 +279,7 @@ def __init__(self, arguments):
283279
"distributed_lock": True,
284280
"thread_local_lock": False,
285281
**arguments,
286-
},
282+
}
287283
)
288284

289285
def _imports(self):

tests/cache/test_memcached_backend.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import ssl
33
from threading import Thread
44
import time
5+
from unittest import mock
56
from unittest import TestCase
67
import weakref
78

89
import pytest
910

11+
from dogpile.cache import make_region
1012
from dogpile.cache.backends.memcached import GenericMemcachedBackend
1113
from dogpile.cache.backends.memcached import MemcachedBackend
1214
from dogpile.cache.backends.memcached import PylibmcBackend
@@ -200,6 +202,17 @@ class PyMemcacheSerializerTest(
200202
backend = "dogpile.cache.pymemcache"
201203

202204

205+
class PyMemcacheRetryTest(_NonDistributedMemcachedTest):
206+
backend = "dogpile.cache.pymemcache"
207+
config_args = {
208+
"arguments": {
209+
"url": MEMCACHED_URL,
210+
"enable_retry_client": True,
211+
"retry_attempts": 3,
212+
}
213+
}
214+
215+
203216
class MemcachedTest(_NonDistributedMemcachedTest):
204217
backend = "dogpile.cache.memcached"
205218

@@ -327,6 +340,20 @@ def test_set_min_compress_len(self):
327340
backend.set("foo", "bar")
328341
eq_(backend._clients.memcached.canary, [{"min_compress_len": 20}])
329342

343+
def test_pymemcache_enable_retry_client_not_set(self):
344+
with mock.patch("warnings.warn") as warn_mock:
345+
_ = make_region().configure(
346+
"dogpile.cache.pymemcache",
347+
arguments={"url": "foo", "retry_attempts": 2},
348+
)
349+
eq_(
350+
warn_mock.mock_calls[0],
351+
mock.call(
352+
"enable_retry_client is not set; retry options "
353+
"will be ignored"
354+
),
355+
)
356+
330357

331358
class LocalThreadTest(TestCase):
332359
def setUp(self):

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ deps=
3838
{memcached}: python-memcached
3939
{memcached}: python-binary-memcached>=0.29.0
4040
{memcached}: pifpaf>=2.5.0
41-
{memcached}: pymemcache>=3.1.0
41+
{memcached}: pymemcache>=3.5.0
4242
{redis}: redis
4343
{redis}: pifpaf
4444
{redis_sentinel}: redis

0 commit comments

Comments
 (0)