diff --git a/hbd/server/udp.py b/hbd/server/udp.py index 76847a5..b7abf24 100644 --- a/hbd/server/udp.py +++ b/hbd/server/udp.py @@ -351,6 +351,7 @@ def restore_connection_timers(hbdclass, ctx): restored += 1 elif state == hbdclass.Connection.OVERDUE: + host_overdue = host.status() == "overdue" elapsed_overdue = now - conn.statetime remaining = DROPOVERDUE - elapsed_overdue if remaining <= 1: @@ -360,6 +361,9 @@ def restore_connection_timers(hbdclass, ctx): "Marking %s/%s UNKNOWN (overdue %.1f days)", uname, afam, elapsed_overdue / 86400, ) + # A connection dropped to UNKNOWN keeps its alert, like the + # UNKNOWN branch above and on_unknown do. + host_overdue = True else: conn.reset_overdue_timer(remaining, on_unknown) logger.debug( @@ -369,7 +373,7 @@ def restore_connection_timers(hbdclass, ctx): # Ensure the connectivity alert is set — it may be missing if # hbd was shut down before the on_overdue callback had a chance # to record it. - if host.status() == "overdue": + if host_overdue: _set_connectivity_alert(host, afam, "CRITICAL") restored += 1 diff --git a/tests/test_udp_presence.py b/tests/test_udp_presence.py index 260d894..060dc86 100644 --- a/tests/test_udp_presence.py +++ b/tests/test_udp_presence.py @@ -171,3 +171,49 @@ def test_restore_skips_absent_and_sets_no_alert_for_simplex(): c.cancel_overdue_timer() asyncio.run(scenario()) + + +def _past_drop(conn): + conn.state, conn.statetime = C.OVERDUE, time.time() - (udp.DROPOVERDUE + 10) + + +def test_restore_past_drop_window_keeps_alert_single_family(): + ctx = _ctx() + + async def scenario(): + _fresh("pres-drop-single") + _send("pres-drop-single", "127.0.0.1", ctx) + h = hbdclass.Host.hosts["pres-drop-single"] + _past_drop(h.connections["IPv4"]) + udp.restore_connection_timers(hbdclass, ctx) + assert h.connections["IPv4"].state == C.UNKNOWN + assert "connectivity.IPv4" in h.alert_states + + asyncio.run(scenario()) + + +def test_restore_past_drop_window_keeps_alerts_dual_family(): + ctx = _ctx() + + async def scenario(): + h = _dual_stack("pres-drop-dual", ctx) + for c in h.connections.values(): + _past_drop(c) + udp.restore_connection_timers(hbdclass, ctx) + assert {"connectivity.IPv4", "connectivity.IPv6"} <= set(h.alert_states) + + asyncio.run(scenario()) + + +def test_v4_only_host_overdue_is_single_critical(monkeypatch): + events = _events(monkeypatch) + ctx = _ctx() + _fresh("pres-v4only") + _send("pres-v4only", "127.0.0.1", ctx) + h = hbdclass.Host.hosts["pres-v4only"] + on_overdue, _ = udp._make_timer_callbacks("pres-v4only", h, ctx) + asyncio.run(on_overdue(h.connections["IPv4"])) + assert [e for e in events if e[0] == "CRITICAL"] == [("CRITICAL", "IPv4 overdue")] + assert "connectivity.IPv4" in h.alert_states + asyncio.run(on_overdue(h.connections["IPv4"])) + assert len([e for e in events if e[0] == "CRITICAL"]) == 1