Skip to content

Commit da490df

Browse files
4383zzzeek
authored andcommitted
Allow to configure pymemcache's HashClient retrying
These changes allow us to configure pymemcache's HashClient retrying mechanisms by exposing his public params. Without these changes that we are limitated to the default values. Configuring the internal retrying feature of the HashClient can be useful in a HA context to properly setup the failover. The internal retrying feature are a good complement to the retrying wrapper introduced previously. [1] https://github.com/pinterest/pymemcache/blob/071191455363943e7d5919701465455e78bb64ae/pymemcache/client/hash.py#L66-L71 Change-Id: Id7aa3d363d281daa640ca34a325454d4c18a8ff0
1 parent 6c0ddf4 commit da490df

3 files changed

Lines changed: 181 additions & 19 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.. change::
2+
:tags: usecase, memcached
3+
4+
Added support for additional pymemcache ``HashClient`` parameters
5+
``retry_attempts``, ``retry_timeout``, and
6+
``dead_timeout``.
7+
8+
.. seealso::
9+
10+
:paramref:`.PyMemcacheBackend.hashclient_retry_attempts`
11+
12+
:paramref:`.PyMemcacheBackend.hashclient_retry_timeout`
13+
14+
:paramref:`.PyMemcacheBackend.dead_timeout`

dogpile/cache/backends/memcached.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -513,13 +513,14 @@ class PyMemcacheBackend(GenericMemcachedBackend):
513513
514514
.. versionadded:: 1.1.4
515515
516-
:param retry_attempts: how many times to attempt an action before
517-
failing. Must be 1 or above. Defaults to None.
516+
:param retry_attempts: how many times to attempt an action with
517+
pymemcache's retrying wrapper before failing. Must be 1 or above.
518+
Defaults to None.
518519
519520
.. versionadded:: 1.1.4
520521
521522
:param retry_delay: optional int|float, how many seconds to sleep between
522-
each attempt. Defaults to None.
523+
each attempt. Used by the retry wrapper. Defaults to None.
523524
524525
.. versionadded:: 1.1.4
525526
@@ -537,6 +538,22 @@ class PyMemcacheBackend(GenericMemcachedBackend):
537538
538539
.. versionadded:: 1.1.4
539540
541+
:param hashclient_retry_attempts: Amount of times a client should be tried
542+
before it is marked dead and removed from the pool in the HashClient's
543+
internal mechanisms.
544+
545+
.. versionadded:: 1.1.5
546+
547+
:param hashclient_retry_timeout: Time in seconds that should pass between
548+
retry attempts in the HashClient's internal mechanisms.
549+
550+
.. versionadded:: 1.1.5
551+
552+
:param dead_timeout: Time in seconds before attempting to add a node
553+
back in the pool in the HashClient's internal mechanisms.
554+
555+
.. versionadded:: 1.1.5
556+
540557
""" # noqa E501
541558

542559
def __init__(self, arguments):
@@ -551,6 +568,13 @@ def __init__(self, arguments):
551568
self.retry_delay = arguments.get("retry_delay", None)
552569
self.retry_for = arguments.get("retry_for", None)
553570
self.do_not_retry_for = arguments.get("do_not_retry_for", None)
571+
self.hashclient_retry_attempts = arguments.get(
572+
"hashclient_retry_attempts", 2
573+
)
574+
self.hashclient_retry_timeout = arguments.get(
575+
"hashclient_retry_timeout", 1
576+
)
577+
self.dead_timeout = arguments.get("hashclient_dead_timeout", 60)
554578
if (
555579
self.retry_delay is not None
556580
or self.retry_attempts is not None
@@ -571,12 +595,14 @@ def _create_client(self):
571595
"serde": self.serde,
572596
"default_noreply": self.default_noreply,
573597
"tls_context": self.tls_context,
598+
"retry_attempts": self.hashclient_retry_attempts,
599+
"retry_timeout": self.hashclient_retry_timeout,
600+
"dead_timeout": self.dead_timeout,
574601
}
575602
if self.socket_keepalive is not None:
576603
_kwargs.update({"socket_keepalive": self.socket_keepalive})
577604

578605
client = pymemcache.client.hash.HashClient(self.url, **_kwargs)
579-
580606
if self.enable_retry_client:
581607
return pymemcache.client.retrying.RetryingClient(
582608
client,

tests/cache/test_memcached_backend.py

Lines changed: 137 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import pytest
1010

11-
from dogpile.cache import make_region
1211
from dogpile.cache.backends.memcached import GenericMemcachedBackend
1312
from dogpile.cache.backends.memcached import MemcachedBackend
1413
from dogpile.cache.backends.memcached import PylibmcBackend
14+
from dogpile.cache.backends.memcached import PyMemcacheBackend
1515
from . import eq_
16+
from . import is_
1617
from ._fixtures import _GenericBackendTest
1718
from ._fixtures import _GenericMutexTest
1819
from ._fixtures import _GenericSerializerTest
@@ -171,20 +172,6 @@ class BMemcachedSerializerTest(
171172
class PyMemcacheTest(_NonDistributedMemcachedTest):
172173
backend = "dogpile.cache.pymemcache"
173174

174-
def test_pymemcache_enable_retry_client_not_set(self):
175-
with mock.patch("warnings.warn") as warn_mock:
176-
_ = make_region().configure(
177-
"dogpile.cache.pymemcache",
178-
arguments={"url": "foo", "retry_attempts": 2},
179-
)
180-
eq_(
181-
warn_mock.mock_calls[0],
182-
mock.call(
183-
"enable_retry_client is not set; retry options "
184-
"will be ignored"
185-
),
186-
)
187-
188175

189176
class PyMemcacheDistributedWithTimeoutTest(
190177
_DistributedMemcachedWithTimeoutTest
@@ -227,6 +214,136 @@ class PyMemcacheRetryTest(_NonDistributedMemcachedTest):
227214
}
228215

229216

217+
class PyMemcacheArgsTest(TestCase):
218+
# TODO: convert dogpile to be able to use pytest.fixtures (remove
219+
# unittest.TestCase dependency) and use that to set up the mock module
220+
# instead of an explicit method call
221+
def _mock_pymemcache_fixture(self):
222+
self.hash_client = mock.Mock()
223+
self.retrying_client = mock.Mock()
224+
self.pickle_serde = mock.Mock()
225+
pymemcache_module = mock.Mock(
226+
serde=mock.Mock(pickle_serde=self.pickle_serde),
227+
client=mock.Mock(
228+
hash=mock.Mock(HashClient=self.hash_client),
229+
retrying=mock.Mock(RetryingClient=self.retrying_client),
230+
),
231+
)
232+
return mock.patch(
233+
"dogpile.cache.backends.memcached.pymemcache", pymemcache_module
234+
)
235+
236+
def test_pymemcache_hashclient_retry_attempts(self):
237+
config_args = {
238+
"url": "127.0.0.1:11211",
239+
"hashclient_retry_attempts": 4,
240+
}
241+
242+
with self._mock_pymemcache_fixture():
243+
backend = MockPyMemcacheBackend(config_args)
244+
is_(backend._create_client(), self.hash_client())
245+
eq_(
246+
self.hash_client.mock_calls[0],
247+
mock.call(
248+
["127.0.0.1:11211"],
249+
serde=self.pickle_serde,
250+
default_noreply=False,
251+
tls_context=None,
252+
retry_attempts=4,
253+
retry_timeout=1,
254+
dead_timeout=60,
255+
),
256+
)
257+
eq_(self.retrying_client.mock_calls, [])
258+
259+
def test_pymemcache_hashclient_retry_timeout(self):
260+
config_args = {"url": "127.0.0.1:11211", "hashclient_retry_timeout": 4}
261+
with self._mock_pymemcache_fixture():
262+
backend = MockPyMemcacheBackend(config_args)
263+
is_(backend._create_client(), self.hash_client())
264+
eq_(
265+
self.hash_client.mock_calls[0],
266+
mock.call(
267+
["127.0.0.1:11211"],
268+
serde=self.pickle_serde,
269+
default_noreply=False,
270+
tls_context=None,
271+
retry_attempts=2,
272+
retry_timeout=4,
273+
dead_timeout=60,
274+
),
275+
)
276+
eq_(self.retrying_client.mock_calls, [])
277+
278+
def test_pymemcache_hashclient_retry_timeout_w_enable_retry(self):
279+
config_args = {
280+
"url": "127.0.0.1:11211",
281+
"hashclient_retry_timeout": 4,
282+
"enable_retry_client": True,
283+
"retry_attempts": 3,
284+
}
285+
with self._mock_pymemcache_fixture():
286+
backend = MockPyMemcacheBackend(config_args)
287+
is_(backend._create_client(), self.retrying_client())
288+
eq_(
289+
self.hash_client.mock_calls[0],
290+
mock.call(
291+
["127.0.0.1:11211"],
292+
serde=self.pickle_serde,
293+
default_noreply=False,
294+
tls_context=None,
295+
retry_attempts=2,
296+
retry_timeout=4,
297+
dead_timeout=60,
298+
),
299+
)
300+
eq_(
301+
self.retrying_client.mock_calls[0],
302+
mock.call(
303+
self.hash_client(),
304+
attempts=3,
305+
retry_delay=None,
306+
retry_for=None,
307+
do_not_retry_for=None,
308+
),
309+
)
310+
311+
def test_pymemcache_dead_timeout(self):
312+
config_args = {"url": "127.0.0.1:11211", "hashclient_dead_timeout": 4}
313+
with self._mock_pymemcache_fixture():
314+
backend = MockPyMemcacheBackend(config_args)
315+
backend._create_client()
316+
eq_(
317+
self.hash_client.mock_calls,
318+
[
319+
mock.call(
320+
["127.0.0.1:11211"],
321+
serde=self.pickle_serde,
322+
default_noreply=False,
323+
tls_context=None,
324+
retry_attempts=2,
325+
retry_timeout=1,
326+
dead_timeout=4,
327+
)
328+
],
329+
)
330+
eq_(self.retrying_client.mock_calls, [])
331+
332+
def test_pymemcache_enable_retry_client_not_set(self):
333+
config_args = {"url": "127.0.0.1:11211", "retry_attempts": 2}
334+
335+
with self._mock_pymemcache_fixture():
336+
with mock.patch("warnings.warn") as warn_mock:
337+
MockPyMemcacheBackend(config_args)
338+
eq_(
339+
warn_mock.mock_calls[0],
340+
mock.call(
341+
"enable_retry_client is not set; retry options "
342+
"will be ignored"
343+
),
344+
)
345+
346+
230347
class MemcachedTest(_NonDistributedMemcachedTest):
231348
backend = "dogpile.cache.memcached"
232349

@@ -253,6 +370,11 @@ def _create_client(self):
253370
return MockClient(self.url)
254371

255372

373+
class MockPyMemcacheBackend(PyMemcacheBackend):
374+
def _imports(self):
375+
pass
376+
377+
256378
class MockMemcacheBackend(MemcachedBackend):
257379
def _imports(self):
258380
pass

0 commit comments

Comments
 (0)