feat: classify offline and degraded connectivity
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""Pure state transitions for device connectivity health."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol, Sequence
|
||||
|
||||
from app.models.device import DeviceMonitoringPolicy
|
||||
|
||||
|
||||
class ProbeSample(Protocol):
|
||||
"""Packet summary attributes required for health evaluation."""
|
||||
|
||||
sent_count: int
|
||||
received_count: int
|
||||
is_valid: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateDecision:
|
||||
"""Result of evaluating recent probes against a monitoring policy."""
|
||||
|
||||
next_status: str
|
||||
event_type: str | None
|
||||
should_notify: bool
|
||||
reason: str
|
||||
|
||||
|
||||
def _retain(previous_status: str, reason: str) -> StateDecision:
|
||||
"""Return a non-notifying decision that preserves current health."""
|
||||
return StateDecision(
|
||||
next_status=previous_status,
|
||||
event_type=None,
|
||||
should_notify=False,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
|
||||
def _transition(
|
||||
previous_status: str,
|
||||
next_status: str,
|
||||
event_type: str,
|
||||
reason: str,
|
||||
) -> StateDecision:
|
||||
"""Return a transition event only when the persisted status changes."""
|
||||
if previous_status == next_status:
|
||||
return _retain(previous_status, reason)
|
||||
return StateDecision(
|
||||
next_status=next_status,
|
||||
event_type=event_type,
|
||||
should_notify=True,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
|
||||
def _is_valid(sample: ProbeSample) -> bool:
|
||||
"""Reject impossible summaries before they influence device health."""
|
||||
return (
|
||||
sample.is_valid
|
||||
and sample.sent_count > 0
|
||||
and 0 <= sample.received_count <= sample.sent_count
|
||||
)
|
||||
|
||||
|
||||
def evaluate_health(
|
||||
previous_status: str,
|
||||
recent: Sequence[ProbeSample],
|
||||
policy: DeviceMonitoringPolicy,
|
||||
) -> StateDecision:
|
||||
"""Evaluate ordered oldest-to-newest probe summaries.
|
||||
|
||||
Invalid input retains state. Offline has priority over aggregate degraded
|
||||
loss, and an alerted state recovers only after the configured clean streak.
|
||||
"""
|
||||
if not recent:
|
||||
return _retain(previous_status, "no probe history")
|
||||
if any(not _is_valid(sample) for sample in recent):
|
||||
return _retain(previous_status, "invalid probe history")
|
||||
|
||||
offline_rounds = policy.offline_consecutive_rounds
|
||||
if len(recent) >= offline_rounds and all(
|
||||
sample.received_count == 0
|
||||
for sample in recent[-offline_rounds:]
|
||||
):
|
||||
return _transition(
|
||||
previous_status,
|
||||
"offline",
|
||||
"offline",
|
||||
f"{offline_rounds} consecutive full-loss rounds",
|
||||
)
|
||||
|
||||
recovery_rounds = policy.recovery_consecutive_clean_rounds
|
||||
if (
|
||||
previous_status in {"offline", "degraded"}
|
||||
and len(recent) >= recovery_rounds
|
||||
and all(
|
||||
sample.received_count == sample.sent_count
|
||||
for sample in recent[-recovery_rounds:]
|
||||
)
|
||||
):
|
||||
return _transition(
|
||||
previous_status,
|
||||
"online",
|
||||
"recovered",
|
||||
f"{recovery_rounds} consecutive clean rounds",
|
||||
)
|
||||
|
||||
degraded_rounds = policy.degraded_window_rounds
|
||||
if len(recent) >= degraded_rounds:
|
||||
window = recent[-degraded_rounds:]
|
||||
sent_count = sum(sample.sent_count for sample in window)
|
||||
received_count = sum(sample.received_count for sample in window)
|
||||
loss_percent = (sent_count - received_count) / sent_count * 100.0
|
||||
if (
|
||||
previous_status != "offline"
|
||||
and loss_percent >= policy.degraded_loss_percent
|
||||
):
|
||||
return _transition(
|
||||
previous_status,
|
||||
"degraded",
|
||||
"degraded",
|
||||
(
|
||||
f"window packet loss {loss_percent:.2f}% is at or above "
|
||||
f"{policy.degraded_loss_percent:.2f}%"
|
||||
),
|
||||
)
|
||||
|
||||
return _retain(previous_status, "no transition threshold met")
|
||||
Reference in New Issue
Block a user