Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion django-backend/soroscan/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response

WORKER_PING_TIMEOUT = 5 # seconds


@api_view(["GET"])
@permission_classes([AllowAny])
Expand Down Expand Up @@ -37,4 +39,28 @@ def readiness_view(request):
if errors:
return Response({"status": "not_ready", "errors": errors}, status=503)

return Response({"status": "ready"})
return Response({"status": "ready"})


@api_view(["GET"])
@permission_classes([AllowAny])
def workers_health_view(request):
"""Check that at least one Celery worker is responding."""
from soroscan.celery import app as celery_app

try:
inspector = celery_app.control.inspect(timeout=WORKER_PING_TIMEOUT)
ping_result = inspector.ping() or {}
except Exception as e:
return Response(
{"status": "error", "detail": f"Celery inspect failed: {e}"},
status=503,
)

if not ping_result:
return Response(
{"status": "no_workers", "detail": "No Celery workers responded"},
status=503,
)

return Response({"status": "ok", "workers": list(ping_result.keys())})
58 changes: 57 additions & 1 deletion django-backend/soroscan/ingest/tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,60 @@ def mocked_get(*args, **kwargs):
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
assert response.data["status"] == "not_ready"
assert any("redis" in e for e in response.data["errors"])
assert response["X-SoroScan-Version"] == settings.SOFTWARE_VERSION
assert response["X-SoroScan-Version"] == settings.SOFTWARE_VERSION


@pytest.mark.django_db
class TestWorkersHealthView:
def test_workers_ok_when_ping_returns_results(self, api_client, monkeypatch):
fake_ping = {"celery@worker1": {"ok": "pong"}}

class FakeInspector:
def ping(self):
return fake_ping

class FakeControl:
def inspect(self, timeout=None):
return FakeInspector()

import soroscan.celery as celery_module
monkeypatch.setattr(celery_module.app, "control", FakeControl())

url = reverse("workers-health")
response = api_client.get(url)

assert response.status_code == status.HTTP_200_OK
assert response.data["status"] == "ok"
assert "celery@worker1" in response.data["workers"]

def test_workers_503_when_no_workers_respond(self, api_client, monkeypatch):
class FakeInspector:
def ping(self):
return {}

class FakeControl:
def inspect(self, timeout=None):
return FakeInspector()

import soroscan.celery as celery_module
monkeypatch.setattr(celery_module.app, "control", FakeControl())

url = reverse("workers-health")
response = api_client.get(url)

assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
assert response.data["status"] == "no_workers"

def test_workers_503_when_inspect_raises(self, api_client, monkeypatch):
class FakeControl:
def inspect(self, timeout=None):
raise Exception("Broker unreachable")

import soroscan.celery as celery_module
monkeypatch.setattr(celery_module.app, "control", FakeControl())

url = reverse("workers-health")
response = api_client.get(url)

assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
assert response.data["status"] == "error"
3 changes: 2 additions & 1 deletion django-backend/soroscan/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

from soroscan.graphql_views import ThrottledGraphQLView
from soroscan.health import health_view, readiness_view
from soroscan.health import health_view, readiness_view, workers_health_view
from soroscan.meta_views import db_pool_stats_view
from soroscan.ingest.views import audit_trail_view, contract_status, rate_limit_analytics_view
from soroscan.ingest.schema import schema
Expand All @@ -34,6 +34,7 @@

path("health/", health_view, name="health"),
path("ready/", readiness_view, name="readiness"),
path("api/health/workers/", workers_health_view, name="workers-health"),

path("admin/", admin.site.urls),
path("api/audit-trail/", audit_trail_view, name="audit-trail"),
Expand Down
Loading