Skip to content

Commit 7fda91f

Browse files
maelnzzzeek
authored andcommitted
Add support for Redis Cluster
Added a new backend :class:`.RedisClusterBackend`, allowing support for Redis Cluster. Pull request courtesy Maël Naccache Tüfekçi. Closes: #250 Pull-request: #250 Pull-request-sha: d112f02 Change-Id: Ifb443567ee963c42e70b145c715b7e37c8660b66
1 parent ba33fd6 commit 7fda91f

File tree

3 files changed

+168
-4
lines changed

3 files changed

+168
-4
lines changed

docs/build/unreleased/250.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.. change::
2+
:tags: feature, redis
3+
:tickets: 250
4+
5+
Added a new backend :class:`.RedisClusterBackend`, allowing support for
6+
Redis Cluster. Pull request courtesy Maël Naccache Tüfekçi.
7+

dogpile/cache/backends/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@
4545
"dogpile.cache.backends.redis",
4646
"RedisSentinelBackend",
4747
)
48+
register_backend(
49+
"dogpile.cache.redis_cluster",
50+
"dogpile.cache.backends.redis",
51+
"RedisClusterBackend",
52+
)

dogpile/cache/backends/redis.py

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
# delayed import
1919
redis = None # noqa F811
2020

21-
__all__ = ("RedisBackend", "RedisSentinelBackend")
21+
__all__ = ("RedisBackend", "RedisSentinelBackend", "RedisClusterBackend")
2222

2323

2424
class RedisBackend(BytesBackend):
2525
r"""A `Redis <http://redis.io/>`_ backend, using the
26-
`redis-py <http://pypi.python.org/pypi/redis/>`_ backend.
26+
`redis-py <http://pypi.python.org/pypi/redis/>`_ driver.
2727
2828
Example configuration::
2929
@@ -227,8 +227,9 @@ def locked(self) -> bool:
227227

228228
class RedisSentinelBackend(RedisBackend):
229229
"""A `Redis <http://redis.io/>`_ backend, using the
230-
`redis-py <http://pypi.python.org/pypi/redis/>`_ backend.
231-
It will use the Sentinel of a Redis cluster.
230+
`redis-py <http://pypi.python.org/pypi/redis/>`_ driver.
231+
This backend is to be used when using
232+
`Redis Sentinel <https://redis.io/docs/management/sentinel/>`_.
232233
233234
.. versionadded:: 1.0.0
234235
@@ -350,3 +351,154 @@ def _create_client(self):
350351
)
351352
self.writer_client = sentinel.master_for(self.service_name)
352353
self.reader_client = sentinel.slave_for(self.service_name)
354+
355+
356+
class RedisClusterBackend(RedisBackend):
357+
r"""A `Redis <http://redis.io/>`_ backend, using the
358+
`redis-py <http://pypi.python.org/pypi/redis/>`_ driver.
359+
This backend is to be used when connecting to a
360+
`Redis Cluster <https://redis.io/docs/management/scaling/>`_ which
361+
will use the
362+
`RedisCluster Client
363+
<https://redis.readthedocs.io/en/stable/connections.html#cluster-client>`_.
364+
365+
.. seealso::
366+
367+
`Clustering <https://redis.readthedocs.io/en/stable/clustering.html>`_
368+
in the redis-py documentation.
369+
370+
Requires redis-py version >=4.1.0.
371+
372+
.. versionadded:: 1.3.2
373+
374+
Connecting to the cluster requires one of:
375+
376+
* Passing a list of startup nodes
377+
* Passing only one node of the cluster, Redis will use automatic discovery
378+
to find the other nodes.
379+
380+
Example configuration, using startup nodes::
381+
382+
from dogpile.cache import make_region
383+
from redis.cluster import ClusterNode
384+
385+
region = make_region().configure(
386+
'dogpile.cache.redis_cluster',
387+
arguments = {
388+
"startup_nodes": [
389+
ClusterNode('localhost', 6379),
390+
ClusterNode('localhost', 6378)
391+
]
392+
}
393+
)
394+
395+
It is recommended to use startup nodes, so that connections will be
396+
successful as at least one node will always be present. Connection
397+
arguments such as password, username or
398+
CA certificate may be passed using ``connection_kwargs``::
399+
400+
from dogpile.cache import make_region
401+
from redis.cluster import ClusterNode
402+
403+
connection_kwargs = {
404+
"username": "admin",
405+
"password": "averystrongpassword",
406+
"ssl": True,
407+
"ssl_ca_certs": "redis.pem",
408+
}
409+
410+
nodes = [
411+
ClusterNode("localhost", 6379),
412+
ClusterNode("localhost", 6380),
413+
ClusterNode("localhost", 6381),
414+
]
415+
416+
region = make_region().configure(
417+
"dogpile.cache.redis_cluster",
418+
arguments={
419+
"startup_nodes": nodes,
420+
"connection_kwargs": connection_kwargs,
421+
},
422+
)
423+
424+
Passing a URL to one node only will allow the driver to discover the whole
425+
cluster automatically::
426+
427+
from dogpile.cache import make_region
428+
429+
region = make_region().configure(
430+
'dogpile.cache.redis_cluster',
431+
arguments = {
432+
"url": "localhost:6379/0"
433+
}
434+
)
435+
436+
A caveat of the above approach is that if the single node targeting
437+
is not available, this would prevent the connection from being successful.
438+
439+
Parameters accepted include:
440+
441+
:param startup_nodes: List of ClusterNode. The list of nodes in
442+
the cluster that the client will try to connect to.
443+
444+
:param url: string. If provided, will override separate
445+
host/password/port/db params. The format is that accepted by
446+
``RedisCluster.from_url()``.
447+
448+
:param db: integer, default is ``0``.
449+
450+
:param redis_expiration_time: integer, number of seconds after setting
451+
a value that Redis should expire it. This should be larger than dogpile's
452+
cache expiration. By default no expiration is set.
453+
454+
:param distributed_lock: boolean, when True, will use a
455+
redis-lock as the dogpile lock. Use this when multiple processes will be
456+
talking to the same redis instance. When left at False, dogpile will
457+
coordinate on a regular threading mutex.
458+
459+
:param lock_timeout: integer, number of seconds after acquiring a lock that
460+
Redis should expire it. This argument is only valid when
461+
``distributed_lock`` is ``True``.
462+
463+
:param socket_timeout: float, seconds for socket timeout.
464+
Default is None (no timeout).
465+
466+
:param lock_sleep: integer, number of seconds to sleep when failed to
467+
acquire a lock. This argument is only valid when
468+
``distributed_lock`` is ``True``.
469+
470+
:param thread_local_lock: bool, whether a thread-local Redis lock object
471+
should be used. This is the default, but is not compatible with
472+
asynchronous runners, as they run in a different thread than the one
473+
used to create the lock.
474+
475+
:param connection_kwargs: dict, additional keyword arguments are passed
476+
along to the
477+
``RedisCluster.from_url()`` method or ``RedisCluster()`` constructor
478+
directly, including parameters like ``ssl``, ``ssl_certfile``,
479+
``charset``, etc.
480+
481+
"""
482+
483+
def __init__(self, arguments):
484+
arguments = arguments.copy()
485+
self.startup_nodes = arguments.pop("startup_nodes", None)
486+
super().__init__(arguments)
487+
488+
def _imports(self):
489+
global redis
490+
import redis.cluster
491+
492+
def _create_client(self):
493+
redis_cluster: redis.cluster.RedisCluster[typing.Any]
494+
if self.url is not None:
495+
redis_cluster = redis.cluster.RedisCluster.from_url(
496+
self.url, **self.connection_kwargs
497+
)
498+
else:
499+
redis_cluster = redis.cluster.RedisCluster(
500+
startup_nodes=self.startup_nodes,
501+
**self.connection_kwargs,
502+
)
503+
self.writer_client = typing.cast(redis.Redis[bytes], redis_cluster)
504+
self.reader_client = self.writer_client

0 commit comments

Comments
 (0)