redis: Support fallback servers

Sentinel clients can use fallback sentinel services so that clients can
obtain available instances even if some of the sentinel services are
unavailable.

Change-Id: Ibdf8993f7ca912bd5acf75ffbc89cf928b607b99
This commit is contained in:
Takashi Kajinami 2024-02-04 21:30:10 +09:00
parent 3fbd05078f
commit fe74dae2fe
3 changed files with 46 additions and 0 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
The redis driver now supports ``sentinel_fallbacks`` option. This allows
using additional sentinel servers as fallbacks.

View File

@ -17,6 +17,7 @@
import contextlib import contextlib
import datetime import datetime
import functools import functools
import re
import string import string
import threading import threading
import time import time
@ -558,6 +559,18 @@ return cmsgpack.pack(result)
ut-were-afraid-to-ask ut-were-afraid-to-ask
""" """
@classmethod
def _parse_sentinel(cls, sentinel):
# IPv6 (eg. [::1]:6379 )
match = re.search(r'\[(\S+)\]:(\d+)', sentinel)
if match:
return (match[1], int(match[2]))
# IPv4 or hostname (eg. 127.0.0.1:6379 or localhost:6379)
match = re.search(r'(\S+):(\d+)', sentinel)
if match:
return (match[1], int(match[2]))
raise ValueError('Malformed sentinel server format')
@classmethod @classmethod
def _make_client(cls, conf): def _make_client(cls, conf):
client_conf = {} client_conf = {}
@ -569,6 +582,8 @@ return cmsgpack.pack(result)
client_conf[key] = conf[key] client_conf[key] = conf[key]
if conf.get('sentinel') is not None: if conf.get('sentinel') is not None:
sentinels = [(client_conf.pop('host'), client_conf.pop('port'))] sentinels = [(client_conf.pop('host'), client_conf.pop('port'))]
for fallback in conf.get('sentinel_fallbacks', []):
sentinels.append(cls._parse_sentinel(fallback))
sentinel_kwargs = conf.get('sentinel_kwargs', {}) sentinel_kwargs = conf.get('sentinel_kwargs', {})
for key in ('username', 'password', 'socket_timeout'): for key in ('username', 'password', 'socket_timeout'):
if key in conf: if key in conf:

View File

@ -146,6 +146,32 @@ class RedisJobboardTest(test.TestCase, base.BoardTestMixin):
**test_conf) **test_conf)
mock_sentinel().master_for.assert_called_once_with('mymaster') mock_sentinel().master_for.assert_called_once_with('mymaster')
def test__make_client_sentinel_fallbacks(self):
conf = {'host': '127.0.0.1',
'port': 26379,
'username': 'default',
'password': 'secret',
'namespace': 'test',
'sentinel': 'mymaster',
'sentinel_fallbacks': [
'[::1]:26379', '127.0.0.2:26379', 'localhost:26379'
]}
with mock.patch('redis.sentinel.Sentinel') as mock_sentinel:
impl_redis.RedisJobBoard('test-board', conf)
test_conf = {
'username': 'default',
'password': 'secret',
}
mock_sentinel.assert_called_once_with(
[('127.0.0.1', 26379), ('::1', 26379),
('127.0.0.2', 26379), ('localhost', 26379)],
sentinel_kwargs={
'username': 'default',
'password': 'secret'
},
**test_conf)
mock_sentinel().master_for.assert_called_once_with('mymaster')
def test__make_client_sentinel_ssl(self): def test__make_client_sentinel_ssl(self):
conf = {'host': '127.0.0.1', conf = {'host': '127.0.0.1',
'port': 26379, 'port': 26379,