diff --git a/hbd/server/flap.py b/hbd/server/flap.py index e1eaa34..e1cc4cb 100644 --- a/hbd/server/flap.py +++ b/hbd/server/flap.py @@ -100,6 +100,17 @@ def observe(host: str, service: str, level: str) -> str: return SUPPRESS +def clear_host(host: str) -> None: + """Discard all flap state for *host* (called when a host is dropped). + + A dropped host may be mid-flap with no RECOVER ever received, in which + case ``ok_since`` stays ``None`` and ``_sweep`` can never clear it on its + own — the state would otherwise persist forever. + """ + for key in [k for k in _state if k[0] == host]: + del _state[key] + + def flapping_services(host: str) -> list: """Return the services of *host* that are currently flapping. diff --git a/hbd/server/http.py b/hbd/server/http.py index 34294b9..2005ea8 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -14,6 +14,7 @@ import logging from aiohttp import web import jinja2 from . import data +from . import flap as flap_mod from . import notify as notify_mod from . import settings as settings_mod from . import users as users_mod @@ -439,6 +440,7 @@ async def start( return web.json_response({"error": "Forbidden"}, status=403) eventlog(uname, "INFO", "dropped") del hbdclass.Host.hosts[uname] + flap_mod.clear_host(uname) return web.Response(text="Done") async def register(request): diff --git a/tests/test_flap.py b/tests/test_flap.py index 72e6eca..2433a77 100644 --- a/tests/test_flap.py +++ b/tests/test_flap.py @@ -98,6 +98,26 @@ def test_alert_during_the_quiet_window_keeps_it_flapping(): assert flap.flapping_services("h1") == ["cpu"] +# --- host removal ------------------------------------------------------------ + +def test_clear_host_drops_flapping_state_even_without_a_recover(): + # A host dropped mid-flap (no RECOVER ever received) must not stay + # flapping forever: nothing will ever set ok_since for it again. + alerts(4) + assert flap.flapping_services("h1") == ["cpu"] + flap.clear_host("h1") + assert flap.flapping_services("h1") == [] + assert flap.observe("h1", "cpu", "CRITICAL") == flap.PASS + + +def test_clear_host_only_affects_the_named_host(): + alerts(4, host="h1") + alerts(4, host="h2") + flap.clear_host("h1") + assert flap.flapping_services("h1") == [] + assert flap.flapping_services("h2") == ["cpu"] + + # --- keying ----------------------------------------------------------------- def test_services_and_hosts_are_tracked_independently():