|
18 | 18 | # delayed import |
19 | 19 | redis = None # noqa F811 |
20 | 20 |
|
21 | | -__all__ = ("RedisBackend", "RedisSentinelBackend") |
| 21 | +__all__ = ("RedisBackend", "RedisSentinelBackend", "RedisClusterBackend") |
22 | 22 |
|
23 | 23 |
|
24 | 24 | class RedisBackend(BytesBackend): |
25 | 25 | 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. |
27 | 27 |
|
28 | 28 | Example configuration:: |
29 | 29 |
|
@@ -227,8 +227,9 @@ def locked(self) -> bool: |
227 | 227 |
|
228 | 228 | class RedisSentinelBackend(RedisBackend): |
229 | 229 | """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/>`_. |
232 | 233 |
|
233 | 234 | .. versionadded:: 1.0.0 |
234 | 235 |
|
@@ -350,3 +351,154 @@ def _create_client(self): |
350 | 351 | ) |
351 | 352 | self.writer_client = sentinel.master_for(self.service_name) |
352 | 353 | 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