Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion server/messaging/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ def apply_timezone(data, fields):
# ===============================================================================
# REPORTING
# ===============================================================================
MAX_ALERT_DOWN_MINUTES = 100 * 365 * 24 * 60


def get_alert_down_minutes():
"""Return NTFPRCS_alert_down_time as whole minutes clamped to
0..MAX_ALERT_DOWN_MINUTES; a non-numeric value counts as 0."""
try:
minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0)
except (TypeError, ValueError):
return 0
return max(0, min(minutes, MAX_ALERT_DOWN_MINUTES))


def get_notifications(db):
"""
Fetch notifications for all configured sections.
Expand Down Expand Up @@ -129,7 +142,7 @@ def get_notifications(db):

# Computed once and shared with NotificationInstance.clearPendingEmailFlag()
# via conf, so the clear step acts on exactly the events selected here.
alert_down_minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0)
alert_down_minutes = get_alert_down_minutes()
alert_down_cutoff = (
timeNowUTC(as_string=False) - timedelta(minutes=alert_down_minutes)
).strftime(DATETIME_PATTERN)
Expand Down
3 changes: 2 additions & 1 deletion server/models/notification_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ def setAllProcessed(self):
def clearPendingEmailFlag(self):
"""Clear evePendingAlertEmail on reported Events and stamp
devLastNotification on their devices; 'Device Down' events are only
cleared once NTFPRCS_alert_down_time minutes have passed."""
cleared if older than conf.down_alert_cutoff, the cutoff
get_notifications() used when selecting them."""

# Clean Pending Alert Events
self.db.sql.execute("""
Expand Down
40 changes: 40 additions & 0 deletions test/backend/test_notification_down_clear_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,5 +275,45 @@ def test_get_notifications_records_cutoff_used_in_query(self):
self.assertLessEqual(cutoff_dt, after - timedelta(minutes=5))


class TestAlertDownTimeBounds(unittest.TestCase):
"""An out-of-range NTFPRCS_alert_down_time must never raise out of
get_notifications() (it runs inside the main monitoring loop)."""

def _run_get_notifications(self, raw_minutes):
"""Run get_notifications() with the given raw setting value and return
the recorded cutoff as a UTC datetime, restoring conf afterward."""
mock_db = Mock()
mock_json_obj = Mock()
mock_json_obj.columnNames = []
mock_json_obj.json = {"data": []}
mock_db.get_table_as_json.return_value = mock_json_obj
settings = {
"NTFPRCS_INCLUDED_SECTIONS": ["down_devices"],
"NTFPRCS_alert_down_time": raw_minutes,
}
original_cutoff = conf.down_alert_cutoff
try:
with patch(
"messaging.reporting.get_setting_value",
side_effect=lambda key: settings.get(key, ""),
):
get_notifications(mock_db)
cutoff = conf.down_alert_cutoff
finally:
conf.down_alert_cutoff = original_cutoff
return datetime.strptime(cutoff, DATETIME_PATTERN).replace(tzinfo=timezone.utc)

def test_huge_value_does_not_raise(self):
"""A value too large for timedelta/datetime must be bounded, not raise
OverflowError."""
cutoff = self._run_get_notifications("999999999999999")
self.assertLess(cutoff, timeNowUTC(as_string=False))

def test_negative_value_does_not_produce_future_cutoff(self):
"""A negative window must not push the cutoff into the future."""
cutoff = self._run_get_notifications("-5")
self.assertLessEqual(cutoff, timeNowUTC(as_string=False))


if __name__ == "__main__":
unittest.main()
Loading