[IMP] maintenance_service_http_monitoring : speed up KO confirmation
Some checks failed
pre-commit / pre-commit (pull_request) Has been cancelled

The previous design required two passes of the 10-minute sweep before a
maintenance request was created, so an outage took at least ~10 extra
minutes (up to 20) to be reported.

Split the monitoring into two crons with distinct responsibilities:

- the 10-minute sweep only discovers outages, flags the KO streak via
  ``http_first_ko_at`` and auto-resolves recovered services;
- a new 2-minute confirmation cron re-checks only the flagged services
  and creates a request once the service has been continuously KO for
  ``HTTP_KO_CONFIRMATION_DELAY`` (lowered from 5 to 2 minutes).

This keeps the transient-outage filter while removing the extra sweep
latency. Services already under an open request are skipped by the
confirmation cron and remain handled by the sweep.
This commit is contained in:
Stéphan Sainléger
2026-09-18 23:13:26 +02:00
parent e48a3443f1
commit 0e32152278
4 changed files with 130 additions and 29 deletions

View File

@@ -12,8 +12,8 @@ EQUIPMENT_HTTP_REQUESTS = (
".models.maintenance_equipment.http_requests"
)
# Comfortably past HTTP_KO_CONFIRMATION_DELAY (currently 10 minutes).
PAST_CONFIRMATION_DELAY = timedelta(minutes=20)
# Comfortably past HTTP_KO_CONFIRMATION_DELAY (currently 2 minutes).
PAST_CONFIRMATION_DELAY = timedelta(minutes=10)
def _mock_response(status_code):
@@ -42,7 +42,7 @@ class TestHttpMonitoring(TransactionCase):
)
def _backdate_first_ko(self, *service_instances):
"""Simulate that the current KO streak started long enough ago to be confirmed."""
"""Simulate a KO streak old enough ago to be confirmed."""
for service_instance in service_instances:
service_instance.write(
{"http_first_ko_at": fields.Datetime.now() - PAST_CONFIRMATION_DELAY}
@@ -81,7 +81,7 @@ class TestHttpMonitoring(TransactionCase):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
request = self.service_instance.http_maintenance_request
self.assertTrue(request)
@@ -118,16 +118,19 @@ class TestHttpMonitoring(TransactionCase):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services() # confirmed, request created
# confirmed -> request created
self.env["service.instance"].cron_confirm_http_ko_services()
request_1 = self.service_instance.http_maintenance_request
self.assertTrue(request_1)
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services() # still KO
# already handled -> skipped
self.env["service.instance"].cron_confirm_http_ko_services()
# Service already has an open request -> the confirmation cron skips it
mock_requests.get.assert_not_called()
self.assertEqual(self.service_instance.http_maintenance_request, request_1)
self.assertEqual(
self.env["maintenance.request"].search_count(
@@ -222,7 +225,7 @@ class TestHttpMonitoring(TransactionCase):
):
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
mock_http.post.assert_called_once()
call_kwargs = mock_http.post.call_args
@@ -249,7 +252,7 @@ class TestHttpMonitoring(TransactionCase):
):
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
self.assertTrue(self.service_instance.http_maintenance_request)
mock_http.post.assert_not_called()
@@ -283,7 +286,7 @@ class TestHttpMonitoring(TransactionCase):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(200)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
self.assertTrue(self.service_instance.http_status_ok)
self.assertEqual(self.service_instance.last_http_status_code, 200)
@@ -314,7 +317,7 @@ class TestHttpMonitoring(TransactionCase):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(503)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
self.assertEqual(mock_requests.get.call_count, 1)
self.assertFalse(self.service_instance.http_status_ok)
@@ -345,7 +348,7 @@ class TestHttpMonitoring(TransactionCase):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
req1 = self.service_instance.http_maintenance_request
req2 = service_instance2.http_maintenance_request
@@ -374,11 +377,11 @@ class TestHttpMonitoring(TransactionCase):
self._backdate_first_ko(self.service_instance)
# Second cron run: streak confirmed -> request created
# Confirmation cron run: streak confirmed -> request created
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.env["service.instance"].cron_confirm_http_ko_services()
request = self.service_instance.http_maintenance_request
self.assertTrue(request)
@@ -411,3 +414,55 @@ class TestHttpMonitoring(TransactionCase):
# Calling close directly must not raise
self.service_instance._close_http_maintenance_request()
self.assertFalse(self.service_instance.http_maintenance_request)
# ------------------------------------------------------------------
# Test 17 -- Confirmation cron ignores services without a KO streak
# ------------------------------------------------------------------
def test_confirmation_cron_ignores_unflagged_services(self):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(200)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_confirm_http_ko_services()
mock_requests.get.assert_not_called()
self.assertFalse(self.service_instance.http_maintenance_request)
# ------------------------------------------------------------------
# Test 18 -- Confirmation cron does not create a request before
# HTTP_KO_CONFIRMATION_DELAY has elapsed
# ------------------------------------------------------------------
def test_confirmation_cron_waits_for_confirmation_delay(self):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.assertTrue(self.service_instance.http_first_ko_at)
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_confirm_http_ko_services()
self.assertFalse(self.service_instance.http_maintenance_request)
# ------------------------------------------------------------------
# Test 19 -- Confirmation cron ignores services whose equipment is in
# maintenance mode
# ------------------------------------------------------------------
def test_confirmation_cron_skips_maintenance_mode(self):
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_check_http_services()
self.assertTrue(self.service_instance.http_first_ko_at)
self.equipment.write({"maintenance_mode": True})
with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests:
mock_requests.get.return_value = _mock_response(500)
mock_requests.exceptions.RequestException = Exception
self.env["service.instance"].cron_confirm_http_ko_services()
mock_requests.get.assert_not_called()
self.assertFalse(self.service_instance.http_maintenance_request)