-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Handle Redis connection errors in result consumer #5921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| from __future__ import absolute_import, unicode_literals | ||
|
|
||
| import time | ||
| from contextlib import contextmanager | ||
| from functools import partial | ||
| from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED | ||
|
|
||
|
|
@@ -78,6 +79,11 @@ | |
|
|
||
| E_LOST = 'Connection to Redis lost: Retry (%s/%s) %s.' | ||
|
|
||
| E_RETRY_LIMIT_EXCEEDED = """ | ||
| Retry limit exceeded while trying to reconnect to the Celery redis result \ | ||
| store backend. The Celery application must be restarted. | ||
| """ | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
|
|
@@ -88,6 +94,8 @@ def __init__(self, *args, **kwargs): | |
| super(ResultConsumer, self).__init__(*args, **kwargs) | ||
| self._get_key_for_task = self.backend.get_key_for_task | ||
| self._decode_result = self.backend.decode_result | ||
| self._ensure = self.backend.ensure | ||
| self._connection_errors = self.backend.connection_errors | ||
| self.subscribed_to = set() | ||
|
|
||
| def on_after_fork(self): | ||
|
|
@@ -99,6 +107,31 @@ def on_after_fork(self): | |
| logger.warning(text_t(e)) | ||
| super(ResultConsumer, self).on_after_fork() | ||
|
|
||
| def _reconnect_pubsub(self): | ||
| self._pubsub = None | ||
| self.backend.client.connection_pool.reset() | ||
| # task state might have changed when the connection was down so we | ||
| # retrieve meta for all subscribed tasks before going into pubsub mode | ||
| metas = self.backend.client.mget(self.subscribed_to) | ||
| metas = [meta for meta in metas if meta] | ||
| for meta in metas: | ||
| self.on_state_change(self._decode_result(meta), None) | ||
| self._pubsub = self.backend.client.pubsub( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is a single statement, but the PubSub instance is also created in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's worth it, as you said it's a single statement, but if you insist I can add it. |
||
| ignore_subscribe_messages=True, | ||
| ) | ||
| self._pubsub.subscribe(*self.subscribed_to) | ||
|
|
||
| @contextmanager | ||
| def reconnect_on_error(self): | ||
| try: | ||
| yield | ||
| except self._connection_errors: | ||
| try: | ||
| self._ensure(self._reconnect_pubsub, ()) | ||
| except self._connection_errors: | ||
| logger.critical(E_RETRY_LIMIT_EXCEEDED) | ||
| raise | ||
|
|
||
| def _maybe_cancel_ready_task(self, meta): | ||
| if meta['status'] in states.READY_STATES: | ||
| self.cancel_for(meta['task_id']) | ||
|
|
@@ -124,9 +157,10 @@ def stop(self): | |
|
|
||
| def drain_events(self, timeout=None): | ||
| if self._pubsub: | ||
| message = self._pubsub.get_message(timeout=timeout) | ||
| if message and message['type'] == 'message': | ||
| self.on_state_change(self._decode_result(message['data']), message) | ||
| with self.reconnect_on_error(): | ||
| message = self._pubsub.get_message(timeout=timeout) | ||
| if message and message['type'] == 'message': | ||
| self.on_state_change(self._decode_result(message['data']), message) | ||
| elif timeout: | ||
| time.sleep(timeout) | ||
|
|
||
|
|
@@ -139,13 +173,15 @@ def _consume_from(self, task_id): | |
| key = self._get_key_for_task(task_id) | ||
| if key not in self.subscribed_to: | ||
| self.subscribed_to.add(key) | ||
| self._pubsub.subscribe(key) | ||
| with self.reconnect_on_error(): | ||
| self._pubsub.subscribe(key) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we could define a context manager that handles the reconnection process.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
|
|
||
| def cancel_for(self, task_id): | ||
| key = self._get_key_for_task(task_id) | ||
| self.subscribed_to.discard(key) | ||
| if self._pubsub: | ||
| key = self._get_key_for_task(task_id) | ||
| self.subscribed_to.discard(key) | ||
| self._pubsub.unsubscribe(key) | ||
| with self.reconnect_on_error(): | ||
| self._pubsub.unsubscribe(key) | ||
|
|
||
|
|
||
| class RedisBackend(BaseKeyValueStoreBackend, AsyncBackendMixin): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use
mgetwith channels?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No we can't. This uses the fact that
RedisBackend.setnot only publishes the task status update to the channel, but also assigns it to a key with the same name. Seecelery/celery/backends/redis.py
Lines 329 to 333 in 90fe53f
In normal operation the consumer listens to messages published to the channel, but when reconnecting it will fetch all values from the keys instead. This is tested since https://github.com/celery/celery/pull/5921/files#diff-5cb790f5c0c0aea16658640ccd102c19R246 is
RedisBackend.setand notRedis.set.