Skip to content
This repository was archived by the owner on Jan 9, 2024. It is now read-only.

Commit ed381f5

Browse files
committed
Setup NullLogging, code lint, Fixed ConnectionError retry code logic
1 parent 83bacbd commit ed381f5

File tree

7 files changed

+232
-53
lines changed

7 files changed

+232
-53
lines changed

docs/logging.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Setup client logging
2+
####################
3+
4+
To setup logging for debugging inside the client during development you can add this as an example to your own code to enable `DEBUG` logging when using the library.
5+
6+
.. code-block:: python
7+
8+
import logging
9+
10+
from rediscluster import RedisCluster
11+
12+
logging.basicConfig()
13+
logger = logging.getLogger('rediscluster')
14+
logger.setLevel(logging.DEBUG)
15+
logger.propergate = True
16+
17+
Note that this logging is not reccommended to be used inside production as it can cause a performance drain and a slowdown of your client.

rediscluster/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
# python std lib
4+
import logging
45
import sys
56

67
# rediscluster imports
@@ -51,3 +52,6 @@ def int_or_str(value):
5152
RedisClusterException,
5253
TryAgainError,
5354
]
55+
56+
# Set default logging handler to avoid "No handler found" warnings.
57+
logging.getLogger(__name__).addHandler(logging.NullHandler())

rediscluster/client.py

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
# python std lib
55
import datetime
6+
import json
7+
import logging
68
import random
79
import string
810
import time
@@ -54,6 +56,9 @@
5456
)
5557

5658

59+
log = logging.getLogger(__name__)
60+
61+
5762
class CaseInsensitiveDict(dict):
5863
"Case insensitive dict implementation. Assumes string keys only."
5964

@@ -320,13 +325,19 @@ def __init__(self, host=None, port=None, startup_nodes=None, max_connections=Non
320325
- db (Redis do not support database SELECT in cluster mode)
321326
"""
322327
# Tweaks to Redis client arguments when running in cluster mode
328+
log.info("Created new instance of RedisCluster client instance")
329+
log.debug("startup_nodes : " + json.dumps(startup_nodes, indent=2))
330+
323331
if "db" in kwargs:
324332
raise RedisClusterException("Argument 'db' is not possible to use in cluster mode")
325333

326-
if kwargs.pop('ssl', False): # Needs to be removed to avoid exception in redis Connection init
334+
# Needs to be removed to avoid exception in redis Connection init
335+
if kwargs.pop('ssl', False):
336+
log.info("Patching connection_class to SSLClusterConnection")
327337
connection_class = SSLClusterConnection
328338

329339
if "connection_pool" in kwargs:
340+
log.info("Using custom created connection pool")
330341
pool = kwargs.pop('connection_pool')
331342
else:
332343
startup_nodes = [] if startup_nodes is None else startup_nodes
@@ -337,10 +348,15 @@ def __init__(self, host=None, port=None, startup_nodes=None, max_connections=Non
337348

338349
if readonly_mode:
339350
connection_pool_cls = ClusterReadOnlyConnectionPool
351+
log.info("Using ClusterReadOnlyConnectionPool")
340352
elif read_from_replicas:
341353
connection_pool_cls = ClusterWithReadReplicasConnectionPool
354+
log.info("Using ClusterWithReadReplicasConnectionPool")
342355
else:
343356
connection_pool_cls = ClusterConnectionPool
357+
log.info("Using ClusterConnectionPool")
358+
359+
log.debug("Connection pool class " + str(connection_pool_cls))
344360

345361
pool = connection_pool_cls(
346362
startup_nodes=startup_nodes,
@@ -545,6 +561,7 @@ def _execute_command(self, *args, **kwargs):
545561
raise RedisClusterException("Unable to determine command to use")
546562

547563
command = args[0]
564+
log.debug("Command to execute : " + str(command) + " : " + str(args) + " : " + str(kwargs))
548565

549566
# If set externally we must update it before calling any commands
550567
if self.refresh_table_asap:
@@ -562,28 +579,34 @@ def _execute_command(self, *args, **kwargs):
562579
try_random_node = False
563580
slot = self._determine_slot(*args)
564581
ttl = int(self.RedisClusterRequestTTL)
582+
connection_error_retry_counter = 0
565583

566584
while ttl > 0:
567585
ttl -= 1
568586

569-
if asking:
570-
node = self.connection_pool.nodes.nodes[redirect_addr]
571-
connection = self.connection_pool.get_connection_by_node(node)
572-
elif try_random_node:
573-
connection = self.connection_pool.get_random_connection()
574-
try_random_node = False
575-
else:
576-
if self.refresh_table_asap:
577-
# MOVED
578-
node = self.connection_pool.get_master_node_by_slot(slot)
579-
# Reset the flag when it has been consumed to avoid it being
580-
self.refresh_table_asap = False
587+
try:
588+
if asking:
589+
node = self.connection_pool.nodes.nodes[redirect_addr]
590+
connection = self.connection_pool.get_connection_by_node(node)
591+
elif try_random_node:
592+
connection = self.connection_pool.get_random_connection()
593+
try_random_node = False
581594
else:
582-
node = self.connection_pool.get_node_by_slot(slot, self.read_from_replicas and (command in self.READ_COMMANDS))
583-
is_read_replica = node['server_type'] == 'slave'
584-
connection = self.connection_pool.get_connection_by_node(node)
595+
if self.refresh_table_asap:
596+
# MOVED
597+
node = self.connection_pool.get_master_node_by_slot(slot)
598+
self.refresh_table_asap = False
599+
else:
600+
node = self.connection_pool.get_node_by_slot(
601+
slot,
602+
self.read_from_replicas and (command in self.READ_COMMANDS)
603+
)
604+
is_read_replica = node['server_type'] == 'slave'
605+
606+
connection = self.connection_pool.get_connection_by_node(node)
607+
608+
log.debug("Determined node to execute : " + str(node))
585609

586-
try:
587610
if asking:
588611
connection.send_command('ASKING')
589612
self.parse_response(connection, "ASKING", **kwargs)
@@ -598,25 +621,53 @@ def _execute_command(self, *args, **kwargs):
598621
connection.send_command(*args)
599622
return self.parse_response(connection, command, **kwargs)
600623
except SlotNotCoveredError as e:
624+
log.exception("SlotNotCoveredError")
625+
601626
# In some cases during failover to a replica is happening
602627
# a slot sometimes is not covered by the cluster layout and
603628
# we need to attempt to refresh the cluster layout and try again
604629
self.refresh_table_asap = True
605-
time.sleep(0.05)
630+
time.sleep(0.1)
606631

607632
# This is the last attempt before we run out of TTL, raise the exception
608633
if ttl == 1:
609634
raise e
610-
except (RedisClusterException, BusyLoadingError):
635+
except (RedisClusterException, BusyLoadingError) as e:
636+
log.exception("RedisClusterException || BusyLoadingError")
611637
raise
612-
except ConnectionError:
638+
except ConnectionError as e:
639+
log.exception("ConnectionError")
640+
613641
connection.disconnect()
614-
except TimeoutError:
642+
connection_error_retry_counter += 1
643+
644+
# Give the node 0.1 seconds to get back up and retry again with same
645+
# node and configuration. After 5 attempts then try to reinitialize
646+
# the cluster and see if the nodes configuration has changed or not
647+
if connection_error_retry_counter < 5:
648+
time.sleep(0.25)
649+
else:
650+
# Reset the counter back to 0 as it should have 5 new attempts
651+
# after the client tries to reinitailize the cluster setup to the
652+
# new configuration.
653+
connection_error_retry_counter = 0
654+
self.refresh_table_asap = True
655+
656+
# Hard force of reinitialize of the node/slots setup
657+
self.connection_pool.nodes.increment_reinitialize_counter(
658+
count=self.connection_pool.nodes.reinitialize_steps,
659+
)
660+
661+
except TimeoutError as e:
662+
log.exception("TimeoutError")
663+
615664
if ttl < self.RedisClusterRequestTTL / 2:
616665
time.sleep(0.05)
617666
else:
618667
try_random_node = True
619668
except ClusterDownError as e:
669+
log.exception("ClusterDownError")
670+
620671
self.connection_pool.disconnect()
621672
self.connection_pool.reset()
622673
self.refresh_table_asap = True
@@ -627,19 +678,27 @@ def _execute_command(self, *args, **kwargs):
627678
# This counter will increase faster when the same client object
628679
# is shared between multiple threads. To reduce the frequency you
629680
# can set the variable 'reinitialize_steps' in the constructor.
681+
log.exception("MovedError")
682+
630683
self.refresh_table_asap = True
631684
self.connection_pool.nodes.increment_reinitialize_counter()
632685

633686
node = self.connection_pool.nodes.set_node(e.host, e.port, server_type='master')
634687
self.connection_pool.nodes.slots[e.slot_id][0] = node
635688
except TryAgainError as e:
689+
log.exception("TryAgainError")
690+
636691
if ttl < self.RedisClusterRequestTTL / 2:
637692
time.sleep(0.05)
638693
except AskError as e:
694+
log.exception("AskError")
695+
639696
redirect_addr, asking = "{0}:{1}".format(e.host, e.port), True
640697
finally:
641698
self.connection_pool.release(connection)
642699

700+
log.debug("TTL loop : " + str(ttl))
701+
643702
raise ClusterError('TTL exhausted.')
644703

645704
def _execute_command_on_nodes(self, nodes, *args, **kwargs):

rediscluster/connection.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# python std lib
44
from __future__ import unicode_literals
5+
import logging
56
import os
67
import random
78
import threading
@@ -23,6 +24,8 @@
2324
from redis.connection import ConnectionPool, Connection, DefaultParser, SSLConnection, UnixDomainSocketConnection
2425
from redis.exceptions import ConnectionError
2526

27+
log = logging.getLogger(__name__)
28+
2629

2730
class ClusterParser(DefaultParser):
2831
"""
@@ -42,6 +45,9 @@ class ClusterConnection(Connection):
4245
"Manages TCP communication to and from a Redis server"
4346

4447
def __init__(self, *args, **kwargs):
48+
log.info("Createing new ClusterConnection instance")
49+
log.debug(str(args) + " : " + str(kwargs))
50+
4551
self.readonly = kwargs.pop('readonly', False)
4652
kwargs['parser_class'] = ClusterParser
4753
super(ClusterConnection, self).__init__(*args, **kwargs)
@@ -54,6 +60,9 @@ def on_connect(self):
5460
super(ClusterConnection, self).on_connect()
5561

5662
if self.readonly:
63+
log.debug("Sending READONLY command to server to configure connection as readonly")
64+
log.debug(str(self))
65+
5766
self.send_command('READONLY')
5867

5968
if nativestr(self.read_response()) != 'OK':
@@ -68,7 +77,10 @@ class SSLClusterConnection(SSLConnection):
6877
client = RedisCluster(connection_pool=pool)
6978
"""
7079

71-
def __init__(self, **kwargs):
80+
def __init__(self, *args, **kwargs):
81+
log.info("Createing new SSLClusterConnection instance")
82+
log.debug(str(args) + " : " + str(kwargs))
83+
7284
self.readonly = kwargs.pop('readonly', False)
7385
# need to pop this off as the redis/connection.py SSLConnection init doesn't work with ssl passed in
7486
if 'ssl' in kwargs:
@@ -84,6 +96,8 @@ def on_connect(self):
8496
super(SSLClusterConnection, self).on_connect()
8597

8698
if self.readonly:
99+
log.debug("Sending READONLY command to server to configure connection as readonly")
100+
87101
self.send_command('READONLY')
88102

89103
if nativestr(self.read_response()) != 'OK':
@@ -109,8 +123,11 @@ def __init__(self, startup_nodes=None, init_slot_cache=True, connection_class=No
109123
it was operating on. This will allow the client to drift along side the cluster
110124
if the cluster nodes move around a lot.
111125
"""
126+
log.info("Creating new ClusterConnectionPool instance")
127+
112128
if connection_class is None:
113129
connection_class = ClusterConnection
130+
114131
super(ClusterConnectionPool, self).__init__(connection_class=connection_class, max_connections=max_connections)
115132

116133
# Special case to make from_url method compliant with cluster setting.
@@ -153,7 +170,10 @@ def __repr__(self):
153170
"""
154171
Return a string with all unique ip:port combinations that this pool is connected to.
155172
"""
156-
nodes = [{'host': i['host'], 'port': i['port']} for i in self.nodes.startup_nodes]
173+
nodes = [
174+
{'host': i['host'], 'port': i['port']}
175+
for i in self.nodes.startup_nodes
176+
]
157177

158178
return "{0}<{1}>".format(
159179
type(self).__name__,
@@ -164,6 +184,8 @@ def reset(self):
164184
"""
165185
Resets the connection pool back to a clean state.
166186
"""
187+
log.debug("Resetting ConnectionPool")
188+
167189
self.pid = os.getpid()
168190
self._created_connections = 0
169191
self._created_connections_per_node = {} # Dict(Node, Int)

0 commit comments

Comments
 (0)