Skip to content

Commit

Permalink
Merged new sentinel commands from #834 (#1550)
Browse files Browse the repository at this point in the history
* Merged new sentinel commands from #835

Thanks you @otherpirate for the contribution!

* 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
  • Loading branch information
chayim authored Aug 29, 2021
1 parent 8cfea41 commit 7c77883
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 5 deletions.
4 changes: 4 additions & 0 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 52 additions & 1 deletion redis/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -2993,3 +2993,54 @@ 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, once=True)

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,
once=True)

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')
21 changes: 19 additions & 2 deletions redis/sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 21 additions & 2 deletions tests/test_sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ 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,
Expand All @@ -42,7 +48,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()
Expand Down Expand Up @@ -198,3 +204,16 @@ 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')

0 comments on commit 7c77883

Please sign in to comment.