Fix worker config refresh and frontend healthcheck

Refresh runtime configuration before worker iterations so long-running worker processes do not keep stale lru_cache values after settings changes. Also probe the frontend healthcheck through 127.0.0.1 to match the nginx listener.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funnywolf
2026-06-28 22:24:01 +08:00
co-authored by Copilot
parent c696ecba3f
commit 16854d0c45
5 changed files with 98 additions and 7 deletions
+21
View File
@@ -0,0 +1,21 @@
from unittest.mock import patch
from django.test import SimpleTestCase
from apps.common.worker_runner import _run_once_or_raise
class WorkerRunnerTests(SimpleTestCase):
def test_run_once_refreshes_runtime_config_before_work(self):
calls = []
def run_once():
calls.append("run_once")
return True
with patch("apps.common.worker_runner._refresh_runtime_config_cache") as refresh:
result = _run_once_or_raise("test worker", run_once)
self.assertTrue(result.processed)
refresh.assert_called_once()
self.assertEqual(calls, ["run_once"])
+8 -1
View File
@@ -49,8 +49,15 @@ def _worker_label(worker_name):
return worker_name if worker_name.endswith("worker") else f"{worker_name} worker"
def _refresh_runtime_config_cache():
from apps.settings.runtime_config import invalidate
invalidate()
def _run_once_or_raise(worker_label, run_once):
try:
_refresh_runtime_config_cache()
return _coerce_result(run_once())
except CommandError:
raise
@@ -95,7 +102,7 @@ def run_worker(
try:
while True:
try:
result = _coerce_result(run_once())
result = _run_once_or_raise(worker_label, run_once)
if result.message:
command.stdout.write(result.message)
if _should_sleep(result, sleep_policy):
+19 -3
View File
@@ -5,12 +5,14 @@ from datetime import UTC, timedelta
from django.utils import timezone
from apps.settings.runtime_config import get_elk_config
from apps.settings.runtime_config import get_elk_config, invalidate
from apps.webhook.service import handle_kibana_webhook
from integrations.siem.clients import get_elk_client
logger = logging.getLogger(__name__)
ELK_CLIENT_CONFIG_FIELDS = ("host", "api_key", "verify_certs", "request_timeout_seconds")
@dataclass
class ProcessResult:
@@ -25,7 +27,7 @@ def format_es_time(value):
class ELKActionProcessor:
def __init__(self, *, elk_client=None, index_name=None, interval_seconds=None, size=None):
config = get_elk_config()
config = self.load_config()
self.elk_client = elk_client
self.index_name_override = index_name
self.interval_seconds_override = interval_seconds
@@ -33,8 +35,18 @@ class ELKActionProcessor:
self.index_name = index_name if index_name is not None else config["action_index"]
self.interval_seconds = interval_seconds if interval_seconds is not None else config["action_poll_interval_seconds"]
self.size = size if size is not None else config["action_size"]
self.elk_client_config = self.client_config(config)
self.last_check_time = None
@staticmethod
def load_config():
invalidate("elk")
return get_elk_config()
@staticmethod
def client_config(config):
return tuple(config[field] for field in ELK_CLIENT_CONFIG_FIELDS)
@staticmethod
def parse_hits(hits):
if isinstance(hits, str):
@@ -44,7 +56,11 @@ class ELKActionProcessor:
return hits if isinstance(hits, list) else []
def refresh_config(self):
config = get_elk_config()
config = self.load_config()
elk_client_config = self.client_config(config)
if self.elk_client is None and elk_client_config != self.elk_client_config:
get_elk_client.cache_clear()
self.elk_client_config = elk_client_config
if self.index_name_override is None:
self.index_name = config["action_index"]
if self.interval_seconds_override is None:
+49 -2
View File
@@ -1,11 +1,14 @@
from datetime import timedelta
from unittest.mock import patch
from django.test import SimpleTestCase, TestCase
from django.urls import Resolver404, resolve
from django.utils import timezone
from rest_framework.test import APIClient
from apps.settings.models import RuntimeConfig
from apps.settings.runtime_config import invalidate
from apps.settings.models import RuntimeConfig, SiemElkConfig
from apps.settings.runtime_config import get_elk_config, invalidate
from apps.webhook.elk_actions import ELKActionProcessor
from apps.webhook.schemas import WebhookResult
from apps.webhook.service import (
WebhookRedisError,
@@ -28,6 +31,26 @@ class FailingRedisClient:
raise RuntimeError("redis unavailable")
class FakeElkClient:
def __init__(self):
self.searches = []
def search(self, **kwargs):
self.searches.append(kwargs)
return {
"hits": {
"hits": [
{
"_source": {
"rule": {"name": "rule-without-hits"},
"context": {"hits": []},
}
}
]
}
}
class WebhookServiceTests(TestCase):
def set_stream_maxlen(self, value):
config = RuntimeConfig.get_current()
@@ -108,6 +131,30 @@ class WebhookServiceTests(TestCase):
)
class ELKActionProcessorTests(TestCase):
def test_process_once_refreshes_config_cached_before_settings_change(self):
config = SiemElkConfig.get_current()
config.host = "https://elk.example.com:9200"
config.api_key = "test-api-key"
config.process_alert_from_index_enabled = False
config.save()
invalidate("elk")
elk_client = FakeElkClient()
processor = ELKActionProcessor(elk_client=elk_client, interval_seconds=60)
self.assertFalse(get_elk_config()["process_alert_from_index_enabled"])
config.process_alert_from_index_enabled = True
config.save(update_fields=["process_alert_from_index_enabled", "updated_at"])
end_time = timezone.now()
result = processor.process_once(start_time=end_time - timedelta(seconds=60), end_time=end_time)
self.assertEqual(result.actions, 1)
self.assertEqual(result.skipped, 1)
self.assertEqual(len(elk_client.searches), 1)
class WebhookAPITests(SimpleTestCase):
def setUp(self):
self.client = APIClient()
+1 -1
View File
@@ -35,7 +35,7 @@ services:
asp-asgi:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-check-certificate -qO- https://localhost/ >/dev/null"]
test: ["CMD-SHELL", "wget --no-check-certificate -qO- https://127.0.0.1/ >/dev/null"]
interval: 30s
timeout: 5s
retries: 5