193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
![]() |
import asyncio
|
||
|
|
||
|
from ..util import wait_ok, wait_convert
|
||
|
from ..commands import Redis
|
||
|
from .pool import create_sentinel_pool
|
||
|
|
||
|
|
||
|
async def create_sentinel(sentinels, *, db=None, password=None,
|
||
|
encoding=None, minsize=1, maxsize=10,
|
||
|
ssl=None, timeout=0.2, loop=None):
|
||
|
"""Creates Redis Sentinel client.
|
||
|
|
||
|
`sentinels` is a list of sentinel nodes.
|
||
|
"""
|
||
|
|
||
|
if loop is None:
|
||
|
loop = asyncio.get_event_loop()
|
||
|
|
||
|
pool = await create_sentinel_pool(sentinels,
|
||
|
db=db,
|
||
|
password=password,
|
||
|
encoding=encoding,
|
||
|
minsize=minsize,
|
||
|
maxsize=maxsize,
|
||
|
ssl=ssl,
|
||
|
timeout=timeout,
|
||
|
loop=loop)
|
||
|
return RedisSentinel(pool)
|
||
|
|
||
|
|
||
|
class RedisSentinel:
|
||
|
"""Redis Sentinel client."""
|
||
|
|
||
|
def __init__(self, pool):
|
||
|
self._pool = pool
|
||
|
|
||
|
def close(self):
|
||
|
"""Close client connections."""
|
||
|
self._pool.close()
|
||
|
|
||
|
async def wait_closed(self):
|
||
|
"""Coroutine waiting until underlying connections are closed."""
|
||
|
await self._pool.wait_closed()
|
||
|
|
||
|
@property
|
||
|
def closed(self):
|
||
|
"""True if connection is closed."""
|
||
|
return self._pool.closed
|
||
|
|
||
|
def master_for(self, name):
|
||
|
"""Returns Redis client to master Redis server."""
|
||
|
# TODO: make class configurable
|
||
|
return Redis(self._pool.master_for(name))
|
||
|
|
||
|
def slave_for(self, name):
|
||
|
"""Returns Redis client to slave Redis server."""
|
||
|
# TODO: make class configurable
|
||
|
return Redis(self._pool.slave_for(name))
|
||
|
|
||
|
def execute(self, command, *args, **kwargs):
|
||
|
"""Execute Sentinel command.
|
||
|
|
||
|
It will be prefixed with SENTINEL automatically.
|
||
|
"""
|
||
|
return self._pool.execute(
|
||
|
b'SENTINEL', command, *args, **kwargs)
|
||
|
|
||
|
async def ping(self):
|
||
|
"""Send PING command to Sentinel instance(s)."""
|
||
|
# TODO: add kwargs allowing to pick sentinel to send command to.
|
||
|
return await self._pool.execute(b'PING')
|
||
|
|
||
|
def master(self, name):
|
||
|
"""Returns a dictionary containing the specified masters state."""
|
||
|
fut = self.execute(b'MASTER', name, encoding='utf-8')
|
||
|
return wait_convert(fut, parse_sentinel_master)
|
||
|
|
||
|
def master_address(self, name):
|
||
|
"""Returns a (host, port) pair for the given ``name``."""
|
||
|
fut = self.execute(b'get-master-addr-by-name', name, encoding='utf-8')
|
||
|
return wait_convert(fut, parse_address)
|
||
|
|
||
|
def masters(self):
|
||
|
"""Returns a list of dictionaries containing each master's state."""
|
||
|
fut = self.execute(b'MASTERS', encoding='utf-8')
|
||
|
# TODO: process masters: we can adjust internal state
|
||
|
return wait_convert(fut, parse_sentinel_masters)
|
||
|
|
||
|
def slaves(self, name):
|
||
|
"""Returns a list of slaves for ``name``."""
|
||
|
fut = self.execute(b'SLAVES', name, encoding='utf-8')
|
||
|
return wait_convert(fut, parse_sentinel_slaves_and_sentinels)
|
||
|
|
||
|
def sentinels(self, name):
|
||
|
"""Returns a list of sentinels for ``name``."""
|
||
|
fut = self.execute(b'SENTINELS', name, encoding='utf-8')
|
||
|
return wait_convert(fut, parse_sentinel_slaves_and_sentinels)
|
||
|
|
||
|
def monitor(self, name, ip, port, quorum):
|
||
|
"""Add a new master to Sentinel to be monitored."""
|
||
|
fut = self.execute(b'MONITOR', name, ip, port, quorum)
|
||
|
return wait_ok(fut)
|
||
|
|
||
|
def remove(self, name):
|
||
|
"""Remove a master from Sentinel's monitoring."""
|
||
|
fut = self.execute(b'REMOVE', name)
|
||
|
return wait_ok(fut)
|
||
|
|
||
|
def set(self, name, option, value):
|
||
|
"""Set Sentinel monitoring parameters for a given master."""
|
||
|
fut = self.execute(b"SET", name, option, value)
|
||
|
return wait_ok(fut)
|
||
|
|
||
|
def failover(self, name):
|
||
|
"""Force a failover of a named master."""
|
||
|
fut = self.execute(b'FAILOVER', name)
|
||
|
return wait_ok(fut)
|
||
|
|
||
|
def check_quorum(self, 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.
|
||
|
"""
|
||
|
return self.execute(b'CKQUORUM', name)
|
||
|
|
||
|
|
||
|
SENTINEL_STATE_TYPES = {
|
||
|
'can-failover-its-master': int,
|
||
|
'config-epoch': int,
|
||
|
'down-after-milliseconds': int,
|
||
|
'failover-timeout': int,
|
||
|
'info-refresh': int,
|
||
|
'last-hello-message': int,
|
||
|
'last-ok-ping-reply': int,
|
||
|
'last-ping-reply': int,
|
||
|
'last-ping-sent': int,
|
||
|
'master-link-down-time': int,
|
||
|
'master-port': int,
|
||
|
'num-other-sentinels': int,
|
||
|
'num-slaves': int,
|
||
|
'o-down-time': int,
|
||
|
'pending-commands': int,
|
||
|
'link-pending-commands': int,
|
||
|
'link-refcount': int,
|
||
|
'parallel-syncs': int,
|
||
|
'port': int,
|
||
|
'quorum': int,
|
||
|
'role-reported-time': int,
|
||
|
's-down-time': int,
|
||
|
'slave-priority': int,
|
||
|
'slave-repl-offset': int,
|
||
|
'voted-leader-epoch': int,
|
||
|
'flags': lambda s: frozenset(s.split(',')), # TODO: make flags enum?
|
||
|
}
|
||
|
|
||
|
|
||
|
def pairs_to_dict_typed(response, type_info):
|
||
|
it = iter(response)
|
||
|
result = {}
|
||
|
for key, value in zip(it, it):
|
||
|
if key in type_info:
|
||
|
try:
|
||
|
value = type_info[key](value)
|
||
|
except (TypeError, ValueError):
|
||
|
# if for some reason the value can't be coerced, just use
|
||
|
# the string value
|
||
|
pass
|
||
|
result[key] = value
|
||
|
return result
|
||
|
|
||
|
|
||
|
def parse_sentinel_masters(response):
|
||
|
result = {}
|
||
|
for item in response:
|
||
|
state = pairs_to_dict_typed(item, SENTINEL_STATE_TYPES)
|
||
|
result[state['name']] = state
|
||
|
return result
|
||
|
|
||
|
|
||
|
def parse_sentinel_slaves_and_sentinels(response):
|
||
|
return [pairs_to_dict_typed(item, SENTINEL_STATE_TYPES)
|
||
|
for item in response]
|
||
|
|
||
|
|
||
|
def parse_sentinel_master(response):
|
||
|
return pairs_to_dict_typed(response, SENTINEL_STATE_TYPES)
|
||
|
|
||
|
|
||
|
def parse_address(value):
|
||
|
if value is not None:
|
||
|
return (value[0], int(value[1]))
|