|
@@ -24,24 +24,33 @@ from . import base
|
|
|
try:
|
|
|
import redis
|
|
|
from kombu.transport.redis import get_redis_error_classes
|
|
|
-except ImportError: # pragma: no cover
|
|
|
- redis = None # noqa
|
|
|
+except ImportError: # pragma: no cover
|
|
|
+ redis = None # noqa
|
|
|
get_redis_error_classes = None # noqa
|
|
|
|
|
|
-__all__ = ('RedisBackend',)
|
|
|
+try:
|
|
|
+ from redis import sentinel
|
|
|
+except ImportError:
|
|
|
+ sentinel = None
|
|
|
+
|
|
|
+__all__ = ('RedisBackend', 'SentinelBackend')
|
|
|
|
|
|
E_REDIS_MISSING = """
|
|
|
You need to install the redis library in order to use \
|
|
|
the Redis result store backend.
|
|
|
"""
|
|
|
|
|
|
+E_REDIS_SENTINEL_MISSING = """
|
|
|
+You need to install the redis library with support of \
|
|
|
+sentinel in order to use the Redis result store backend.
|
|
|
+"""
|
|
|
+
|
|
|
E_LOST = 'Connection to Redis lost: Retry (%s/%s) %s.'
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
class ResultConsumer(async.BaseResultConsumer):
|
|
|
-
|
|
|
_pubsub = None
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
@@ -265,12 +274,12 @@ class RedisBackend(base.BaseKeyValueStoreBackend, async.AsyncBackendMixin):
|
|
|
tkey = self.get_key_for_group(gid, '.t')
|
|
|
result = self.encode_result(result, state)
|
|
|
with client.pipeline() as pipe:
|
|
|
- _, readycount, totaldiff, _, _ = pipe \
|
|
|
- .rpush(jkey, self.encode([1, tid, state, result])) \
|
|
|
- .llen(jkey) \
|
|
|
- .get(tkey) \
|
|
|
- .expire(jkey, self.expires) \
|
|
|
- .expire(tkey, self.expires) \
|
|
|
+ _, readycount, totaldiff, _, _ = pipe \
|
|
|
+ .rpush(jkey, self.encode([1, tid, state, result])) \
|
|
|
+ .llen(jkey) \
|
|
|
+ .get(tkey) \
|
|
|
+ .expire(jkey, self.expires) \
|
|
|
+ .expire(tkey, self.expires) \
|
|
|
.execute()
|
|
|
|
|
|
totaldiff = int(totaldiff or 0)
|
|
@@ -281,10 +290,10 @@ class RedisBackend(base.BaseKeyValueStoreBackend, async.AsyncBackendMixin):
|
|
|
if readycount == total:
|
|
|
decode, unpack = self.decode, self._unpack_chord_result
|
|
|
with client.pipeline() as pipe:
|
|
|
- resl, _, _ = pipe \
|
|
|
- .lrange(jkey, 0, total) \
|
|
|
- .delete(jkey) \
|
|
|
- .delete(tkey) \
|
|
|
+ resl, _, _ = pipe \
|
|
|
+ .lrange(jkey, 0, total) \
|
|
|
+ .delete(jkey) \
|
|
|
+ .delete(tkey) \
|
|
|
.execute()
|
|
|
try:
|
|
|
callback.delay([unpack(tup, decode) for tup in resl])
|
|
@@ -306,10 +315,16 @@ class RedisBackend(base.BaseKeyValueStoreBackend, async.AsyncBackendMixin):
|
|
|
)
|
|
|
|
|
|
def _create_client(self, **params):
|
|
|
- return self.redis.StrictRedis(
|
|
|
- connection_pool=self.ConnectionPool(**params),
|
|
|
+ return self._get_client()(
|
|
|
+ connection_pool=self._get_pool(**params),
|
|
|
)
|
|
|
|
|
|
+ def _get_client(self):
|
|
|
+ return self.redis.StrictRedis
|
|
|
+
|
|
|
+ def _get_pool(self, **params):
|
|
|
+ return self.ConnectionPool(**params)
|
|
|
+
|
|
|
@property
|
|
|
def ConnectionPool(self):
|
|
|
if self._ConnectionPool is None:
|
|
@@ -340,3 +355,63 @@ class RedisBackend(base.BaseKeyValueStoreBackend, async.AsyncBackendMixin):
|
|
|
@deprecated.Property(4.0, 5.0)
|
|
|
def password(self):
|
|
|
return self.connparams['password']
|
|
|
+
|
|
|
+
|
|
|
+class SentinelBackend(RedisBackend):
|
|
|
+ """Redis sentinel task result store."""
|
|
|
+
|
|
|
+ sentinel = sentinel
|
|
|
+
|
|
|
+ def __init__(self, *args, **kwargs):
|
|
|
+ if self.sentinel is None:
|
|
|
+ raise ImproperlyConfigured(E_REDIS_SENTINEL_MISSING.strip())
|
|
|
+
|
|
|
+ super(SentinelBackend, self).__init__(*args, **kwargs)
|
|
|
+
|
|
|
+ def _params_from_url(self, url, defaults):
|
|
|
+ # URL looks like sentinel://0.0.0.0:26347/3;sentinel://0.0.0.0:26348/3.
|
|
|
+ chunks = url.split(";")
|
|
|
+ connparams = dict(defaults, hosts=[])
|
|
|
+ for chunk in chunks:
|
|
|
+ data = super(SentinelBackend, self)._params_from_url(
|
|
|
+ url=chunk, defaults=defaults)
|
|
|
+ connparams['hosts'].append(data)
|
|
|
+ for p in ("host", "port", "db", "password"):
|
|
|
+ connparams.pop(p)
|
|
|
+
|
|
|
+ # Adding db/password in connparams to connect to the correct instance
|
|
|
+ for p in ("db", "password"):
|
|
|
+ if connparams['hosts'] and p in connparams['hosts'][0]:
|
|
|
+ connparams[p] = connparams['hosts'][0].get(p)
|
|
|
+ return connparams
|
|
|
+
|
|
|
+ def _get_sentinel_instance(self, **params):
|
|
|
+ connparams = params.copy()
|
|
|
+
|
|
|
+ hosts = connparams.pop("hosts")
|
|
|
+ result_backend_transport_opts = self.app.conf.get(
|
|
|
+ "result_backend_transport_options", {})
|
|
|
+ min_other_sentinels = result_backend_transport_opts.get(
|
|
|
+ "min_other_sentinels", 0)
|
|
|
+ sentinel_kwargs = result_backend_transport_opts.get(
|
|
|
+ "sentinel_kwargs", {})
|
|
|
+
|
|
|
+ sentinel_instance = self.sentinel.Sentinel(
|
|
|
+ [(cp['host'], cp['port']) for cp in hosts],
|
|
|
+ min_other_sentinels=min_other_sentinels,
|
|
|
+ sentinel_kwargs=sentinel_kwargs,
|
|
|
+ **connparams)
|
|
|
+
|
|
|
+ return sentinel_instance
|
|
|
+
|
|
|
+ def _get_pool(self, **params):
|
|
|
+ sentinel_instance = self._get_sentinel_instance(**params)
|
|
|
+
|
|
|
+ result_backend_transport_opts = self.app.conf.get(
|
|
|
+ "result_backend_transport_options", {})
|
|
|
+ master_name = result_backend_transport_opts.get("master_name", None)
|
|
|
+
|
|
|
+ return sentinel_instance.master_for(
|
|
|
+ service_name=master_name,
|
|
|
+ redis_class=self._get_client(),
|
|
|
+ ).connection_pool
|