From ff5ac800fb43453da05771aafd402c27a8e9eee8 Mon Sep 17 00:00:00 2001 From: "Chayim I. Kirshen" Date: Mon, 23 Aug 2021 14:22:33 +0300 Subject: [PATCH 1/3] Merged new sentinel commands from #835 Thanks you @otherpirate for the contribution! --- redis/commands.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/redis/commands.py b/redis/commands.py index 003b0f1bcc..1e9ab597c4 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -2993,3 +2993,52 @@ def sentinel_set(self, name, option, value): def sentinel_slaves(self, service_name): "Returns a list of slaves for ``service_name``" return self.execute_command('SENTINEL SLAVES', service_name) + + def sentinel_reset(self, pattern): + """ + This command will reset all the masters with matching name. + The pattern argument is a glob-style pattern. + + The reset process clears any previous state in a master (including a + failover in progress), and removes every slave and sentinel already + discovered and associated with the master. + """ + return self.execute_command('SENTINEL RESET', pattern) + + def sentinel_failover(self, new_master_name): + """ + Force a failover as if the master was not reachable, and without + asking for agreement to other Sentinels (however a new version of the + configuration will be published so that the other Sentinels will + update their configurations). + """ + return self.execute_command('SENTINEL FAILOVER', new_master_name) + + def sentinel_ckquorum(self, new_master_name): + """ + Check if the current Sentinel configuration is able to reach the + quorum needed to failover a master, and the majority needed to + authorize the failover. + + This command should be used in monitoring systems to check if a + Sentinel deployment is ok. + """ + return self.execute_command('SENTINEL CKQUORUM', new_master_name) + + def sentinel_flushconfig(self): + """ + Force Sentinel to rewrite its configuration on disk, including the + current Sentinel state. + + Normally Sentinel rewrites the configuration every time something + changes in its state (in the context of the subset of the state which + is persisted on disk across restart). + However sometimes it is possible that the configuration file is lost + because of operation errors, disk failures, package upgrade scripts or + configuration managers. In those cases a way to to force Sentinel to + rewrite the configuration file is handy. + + This command works even if the previous configuration file is + completely missing. + """ + return self.execute_command('SENTINEL FLUSHCONFIG') From c058c9c4e0982c143b502ee2cdbd243eb7f0070b Mon Sep 17 00:00:00 2001 From: "Chayim I. Kirshen" Date: Mon, 23 Aug 2021 16:26:07 +0300 Subject: [PATCH 2/3] Added an execute wrapper and tests. The tests ensure that the function is called. Nothing more since we do not currently have enough testing support for sentinel --- redis/client.py | 4 ++++ redis/commands.py | 8 +++++--- redis/sentinel.py | 21 +++++++++++++++++++-- tests/test_sentinel.py | 19 ++++++++++++++++--- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/redis/client.py b/redis/client.py index ab9246d160..f6ca071ed4 100755 --- a/redis/client.py +++ b/redis/client.py @@ -675,10 +675,14 @@ class Redis(Commands, object): 'SCRIPT FLUSH': bool_ok, 'SCRIPT KILL': bool_ok, 'SCRIPT LOAD': str_if_bytes, + 'SENTINEL CKQUORUM': bool_ok, + 'SENTINEL FAILOVER': bool_ok, + 'SENTINEL FLUSHCONFIG': bool_ok, 'SENTINEL GET-MASTER-ADDR-BY-NAME': parse_sentinel_get_master, 'SENTINEL MASTER': parse_sentinel_master, 'SENTINEL MASTERS': parse_sentinel_masters, 'SENTINEL MONITOR': bool_ok, + 'SENTINEL RESET': bool_ok, 'SENTINEL REMOVE': bool_ok, 'SENTINEL SENTINELS': parse_sentinel_slaves_and_sentinels, 'SENTINEL SET': bool_ok, diff --git a/redis/commands.py b/redis/commands.py index 1e9ab597c4..3d5670e1f1 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -2950,7 +2950,7 @@ def execute(self): return self.client.execute_command(*command) -class SentinalCommands: +class SentinelCommands: """ A class containing the commands specific to redis sentinal. This class is to be used as a mixin. @@ -3003,7 +3003,7 @@ def sentinel_reset(self, pattern): failover in progress), and removes every slave and sentinel already discovered and associated with the master. """ - return self.execute_command('SENTINEL RESET', pattern) + return self.execute_command('SENTINEL RESET', pattern, once=True) def sentinel_failover(self, new_master_name): """ @@ -3023,7 +3023,9 @@ def sentinel_ckquorum(self, new_master_name): This command should be used in monitoring systems to check if a Sentinel deployment is ok. """ - return self.execute_command('SENTINEL CKQUORUM', new_master_name) + return self.execute_command('SENTINEL CKQUORUM', + new_master_name, + once=True) def sentinel_flushconfig(self): """ diff --git a/redis/sentinel.py b/redis/sentinel.py index d3213488c1..6456b8e868 100644 --- a/redis/sentinel.py +++ b/redis/sentinel.py @@ -2,7 +2,7 @@ import weakref from redis.client import Redis -from redis.commands import SentinalCommands +from redis.commands import SentinelCommands from redis.connection import ConnectionPool, Connection from redis.exceptions import (ConnectionError, ResponseError, ReadOnlyError, TimeoutError) @@ -133,7 +133,7 @@ def rotate_slaves(self): raise SlaveNotFoundError('No slave found for %r' % (self.service_name)) -class Sentinel(SentinalCommands, object): +class Sentinel(SentinelCommands, object): """ Redis Sentinel cluster client @@ -179,6 +179,23 @@ def __init__(self, sentinels, min_other_sentinels=0, sentinel_kwargs=None, self.min_other_sentinels = min_other_sentinels self.connection_kwargs = connection_kwargs + def execute_command(self, *args, **kwargs): + """ + Execute Sentinel command in sentinel nodes. + once - If set to True, then execute the resulting command on a single + node at random, rather than across the entire sentinel cluster. + """ + once = bool(kwargs.get('once', False)) + if 'once' in kwargs.keys(): + kwargs.pop('once') + + if once: + for sentinel in self.sentinels: + sentinel.execute_command(*args, **kwargs) + else: + random.choice(self.sentinels).execute_command(*args, **kwargs) + return True + def __repr__(self): sentinel_addresses = [] for sentinel in self.sentinels: diff --git a/tests/test_sentinel.py b/tests/test_sentinel.py index 64a7c47d3a..cb599117ec 100644 --- a/tests/test_sentinel.py +++ b/tests/test_sentinel.py @@ -30,9 +30,13 @@ def sentinel_slaves(self, master_name): return [] return self.cluster.slaves + def execute_command(self, *args, **kwargs): + # wrapper purely to validate the calls don't explode + from redis.client import bool_ok + return bool_ok class SentinelTestCluster: - def __init__(self, service_name='mymaster', ip='127.0.0.1', port=6379): + def __init__(self, servisentinel_ce_name='mymaster', ip='127.0.0.1', port=6379): self.clients = {} self.master = { 'ip': ip, @@ -42,7 +46,7 @@ def __init__(self, service_name='mymaster', ip='127.0.0.1', port=6379): 'is_odown': False, 'num-other-sentinels': 0, } - self.service_name = service_name + self.service_name = servisentinel_ce_name self.slaves = [] self.nodes_down = set() self.nodes_timeout = set() @@ -79,7 +83,6 @@ def test_discover_master(sentinel, master_ip): address = sentinel.discover_master('mymaster') assert address == (master_ip, 6379) - def test_discover_master_error(sentinel): with pytest.raises(MasterNotFoundError): sentinel.discover_master('xxx') @@ -198,3 +201,13 @@ def test_slave_round_robin(cluster, sentinel, master_ip): assert next(rotator) == (master_ip, 6379) with pytest.raises(SlaveNotFoundError): next(rotator) + +def test_ckquorum(cluster, sentinel): + assert sentinel.sentinel_ckquorum("mymaster") + +def test_flushconfig(cluster, sentinel): + assert sentinel.sentinel_flushconfig() + +def test_reset(cluster, sentinel): + cluster.master['is_odown'] = True + assert sentinel.sentinel_reset('mymaster') From 7bc202d1fa3c54aba1eceb0e9213e3e54aec2e49 Mon Sep 17 00:00:00 2001 From: "Chayim I. Kirshen" Date: Mon, 23 Aug 2021 16:34:21 +0300 Subject: [PATCH 3/3] flake8 --- tests/test_sentinel.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_sentinel.py b/tests/test_sentinel.py index cb599117ec..54cf262c43 100644 --- a/tests/test_sentinel.py +++ b/tests/test_sentinel.py @@ -35,8 +35,10 @@ def execute_command(self, *args, **kwargs): from redis.client import bool_ok return bool_ok + class SentinelTestCluster: - def __init__(self, servisentinel_ce_name='mymaster', ip='127.0.0.1', port=6379): + def __init__(self, servisentinel_ce_name='mymaster', ip='127.0.0.1', + port=6379): self.clients = {} self.master = { 'ip': ip, @@ -83,6 +85,7 @@ def test_discover_master(sentinel, master_ip): address = sentinel.discover_master('mymaster') assert address == (master_ip, 6379) + def test_discover_master_error(sentinel): with pytest.raises(MasterNotFoundError): sentinel.discover_master('xxx') @@ -202,12 +205,15 @@ def test_slave_round_robin(cluster, sentinel, master_ip): with pytest.raises(SlaveNotFoundError): next(rotator) + def test_ckquorum(cluster, sentinel): assert sentinel.sentinel_ckquorum("mymaster") + def test_flushconfig(cluster, sentinel): assert sentinel.sentinel_flushconfig() + def test_reset(cluster, sentinel): cluster.master['is_odown'] = True assert sentinel.sentinel_reset('mymaster')