-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add second celery health chat that uses ping instead of executing a t…
…ask (#272) Co-authored-by: Witold Greń <[email protected]>
- Loading branch information
1 parent
f5244fb
commit 60ac341
Showing
9 changed files
with
245 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
default_app_config = 'health_check.contrib.celery_ping.apps.HealthCheckConfig' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from django.apps import AppConfig | ||
|
||
from health_check.plugins import plugin_dir | ||
|
||
|
||
class HealthCheckConfig(AppConfig): | ||
name = 'health_check.contrib.celery_ping' | ||
|
||
def ready(self): | ||
from .backends import CeleryPingHealthCheck | ||
|
||
plugin_dir.register(CeleryPingHealthCheck) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from celery.app import default_app as app | ||
from django.conf import settings | ||
|
||
from health_check.backends import BaseHealthCheckBackend | ||
from health_check.exceptions import ServiceUnavailable | ||
|
||
|
||
class CeleryPingHealthCheck(BaseHealthCheckBackend): | ||
CORRECT_PING_RESPONSE = {"ok": "pong"} | ||
|
||
def check_status(self): | ||
timeout = getattr(settings, "HEALTHCHECK_CELERY_PING_TIMEOUT", 1) | ||
|
||
try: | ||
ping_result = app.control.ping(timeout=timeout) | ||
except IOError as e: | ||
self.add_error(ServiceUnavailable("IOError"), e) | ||
except NotImplementedError as exc: | ||
self.add_error( | ||
ServiceUnavailable( | ||
"NotImplementedError: Make sure CELERY_RESULT_BACKEND is set" | ||
), | ||
exc, | ||
) | ||
except BaseException as exc: | ||
self.add_error(ServiceUnavailable("Unknown error"), exc) | ||
else: | ||
if not ping_result: | ||
self.add_error( | ||
ServiceUnavailable("Celery workers unavailable"), | ||
) | ||
else: | ||
self._check_ping_result(ping_result) | ||
|
||
def _check_ping_result(self, ping_result): | ||
active_workers = [] | ||
|
||
for worker, response in ping_result[0].items(): | ||
if response != self.CORRECT_PING_RESPONSE: | ||
self.add_error( | ||
ServiceUnavailable( | ||
f"Celery worker {worker} response was incorrect" | ||
), | ||
) | ||
continue | ||
active_workers.append(worker) | ||
|
||
if not self.errors: | ||
self._check_active_queues(active_workers) | ||
|
||
def _check_active_queues(self, active_workers): | ||
defined_queues = app.conf.CELERY_QUEUES | ||
|
||
if not defined_queues: | ||
return | ||
|
||
defined_queues = set([queue.name for queue in defined_queues]) | ||
active_queues = set() | ||
|
||
for queues in app.control.inspect(active_workers).active_queues().values(): | ||
active_queues.update([queue.get("name") for queue in queues]) | ||
|
||
for queue in defined_queues.difference(active_queues): | ||
self.add_error( | ||
ServiceUnavailable(f"No worker for Celery task queue {queue}"), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import pytest | ||
from django.apps import apps | ||
from django.conf import settings | ||
from mock import patch | ||
|
||
from health_check.contrib.celery_ping.apps import HealthCheckConfig | ||
from health_check.contrib.celery_ping.backends import CeleryPingHealthCheck | ||
|
||
|
||
class TestCeleryPingHealthCheck: | ||
CELERY_APP_CONTROL_PING = ( | ||
"health_check.contrib.celery_ping.backends.app.control.ping" | ||
) | ||
CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES = ( | ||
"health_check.contrib.celery_ping.backends.app.control.inspect.active_queues" | ||
) | ||
|
||
@pytest.fixture | ||
def health_check(self): | ||
return CeleryPingHealthCheck() | ||
|
||
def test_check_status_doesnt_add_errors_when_ping_successfull(self, health_check): | ||
celery_worker = "celery@4cc150a7b49b" | ||
|
||
with patch( | ||
self.CELERY_APP_CONTROL_PING, | ||
return_value=[{celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE}], | ||
), patch( | ||
self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES, | ||
return_value={ | ||
celery_worker: [ | ||
{"name": queue.name} for queue in settings.CELERY_QUEUES | ||
] | ||
}, | ||
): | ||
health_check.check_status() | ||
|
||
assert not health_check.errors | ||
|
||
def test_check_status_reports_errors_if_ping_responses_are_incorrect( | ||
self, health_check | ||
): | ||
with patch( | ||
self.CELERY_APP_CONTROL_PING, | ||
return_value=[ | ||
{ | ||
"celery1@4cc150a7b49b": CeleryPingHealthCheck.CORRECT_PING_RESPONSE, | ||
"celery2@4cc150a7b49b": {}, | ||
"celery3@4cc150a7b49b": {"error": "pong"}, | ||
} | ||
], | ||
): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == 2 | ||
|
||
def test_check_status_adds_errors_when_ping_successfull_but_not_all_defined_queues_have_consumers( | ||
self, | ||
health_check, | ||
): | ||
celery_worker = "celery@4cc150a7b49b" | ||
queues = list(settings.CELERY_QUEUES) | ||
|
||
with patch( | ||
self.CELERY_APP_CONTROL_PING, | ||
return_value=[{celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE}], | ||
), patch( | ||
self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES, | ||
return_value={celery_worker: [{"name": queues.pop().name}]}, | ||
): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == len(queues) | ||
|
||
@pytest.mark.parametrize( | ||
"exception_to_raise", | ||
[ | ||
IOError, | ||
TimeoutError, | ||
], | ||
) | ||
def test_check_status_add_error_when_io_error_raised_from_ping( | ||
self, exception_to_raise, health_check | ||
): | ||
with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == 1 | ||
assert "ioerror" in health_check.errors[0].message.lower() | ||
|
||
@pytest.mark.parametrize( | ||
"exception_to_raise", [ValueError, SystemError, IndexError, MemoryError] | ||
) | ||
def test_check_status_add_error_when_any_exception_raised_from_ping( | ||
self, exception_to_raise, health_check | ||
): | ||
with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == 1 | ||
assert health_check.errors[0].message.lower() == "unknown error" | ||
|
||
def test_check_status_when_raised_exception_notimplementederror(self, health_check): | ||
expected_error_message = ( | ||
"notimplementederror: make sure celery_result_backend is set" | ||
) | ||
|
||
with patch(self.CELERY_APP_CONTROL_PING, side_effect=NotImplementedError): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == 1 | ||
assert health_check.errors[0].message.lower() == expected_error_message | ||
|
||
@pytest.mark.parametrize("ping_result", [None, list()]) | ||
def test_check_status_add_error_when_ping_result_failed( | ||
self, ping_result, health_check | ||
): | ||
with patch(self.CELERY_APP_CONTROL_PING, return_value=ping_result): | ||
health_check.check_status() | ||
|
||
assert len(health_check.errors) == 1 | ||
assert "workers unavailable" in health_check.errors[0].message.lower() | ||
|
||
|
||
class TestCeleryPingHealthCheckApps: | ||
def test_apps(self): | ||
assert HealthCheckConfig.name == "health_check.contrib.celery_ping" | ||
|
||
celery_ping = apps.get_app_config("celery_ping") | ||
assert celery_ping.name == "health_check.contrib.celery_ping" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
from celery import Celery | ||
|
||
app = Celery('testapp') | ||
app = Celery('testapp', broker='memory://') | ||
app.config_from_object('django.conf:settings', namespace='CELERY') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters