Aug 27 - Add MySQL optimize and security check
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Equivalence test for the SQL prefilter in send_sla_alerts().
|
||||
|
||||
The cron used to read EVERY open issue and decide in Python which ones deserved
|
||||
a notification. It now narrows to candidates in SQL first.
|
||||
|
||||
The contract is a CONSERVATIVE SUPERSET, not equality:
|
||||
|
||||
* Nothing the old loop would have notified may be missed. This is the safety
|
||||
property -- a miss is a silently unsent SLA alert.
|
||||
* The SQL may return extra rows, because it deliberately does not replicate
|
||||
the "already notified at_risk and still only at_risk" skip (that would mean
|
||||
writing the per-severity deadline arithmetic a second time, in SQL). The
|
||||
Python loop still applies that skip, so no extra notification is ever sent
|
||||
-- only a handful of extra rows are read.
|
||||
|
||||
The reference predicate below is the old loop's skip logic, written out.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from app import db
|
||||
from app.models.issue import Issue
|
||||
from app.utils.sla import SLA_HOURS, AT_RISK_THRESHOLD, sla_status
|
||||
from app.utils.time_utils import now_eastern
|
||||
|
||||
|
||||
def _candidate_query():
|
||||
"""The prefilter exactly as send_sla_alerts() builds it."""
|
||||
now = now_eastern()
|
||||
age_clauses = [
|
||||
db.and_(
|
||||
Issue.severity == severity,
|
||||
Issue.reported_at <= now - timedelta(hours=hours * AT_RISK_THRESHOLD),
|
||||
)
|
||||
for severity, hours in SLA_HOURS.items()
|
||||
]
|
||||
return Issue.query.filter(
|
||||
Issue.status.in_(['open', 'in_progress', 'pending_verification']),
|
||||
Issue.reported_at.isnot(None),
|
||||
db.or_(Issue.sla_notified.is_(None), Issue.sla_notified != 'breached'),
|
||||
db.or_(*age_clauses),
|
||||
)
|
||||
|
||||
|
||||
def _old_loop_would_act(issue):
|
||||
"""The pre-change Python logic: which rows survived to send a notification."""
|
||||
if issue.status not in ('open', 'in_progress', 'pending_verification'):
|
||||
return False
|
||||
if issue.reported_at is None:
|
||||
return False # would have raised TypeError -- see the NULL test
|
||||
status = sla_status(issue)
|
||||
if status not in ('at_risk', 'breached'):
|
||||
return False
|
||||
already = issue.sla_notified
|
||||
if already == 'breached':
|
||||
return False
|
||||
if already == 'at_risk' and status == 'at_risk':
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _add(severity, hours_ago, status='open', sla_notified=None):
|
||||
issue = Issue(
|
||||
severity=severity,
|
||||
description='x',
|
||||
status=status,
|
||||
sla_notified=sla_notified,
|
||||
reported_at=now_eastern() - timedelta(hours=hours_ago),
|
||||
)
|
||||
db.session.add(issue)
|
||||
return issue
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def population(app):
|
||||
"""One issue per interesting combination of severity / age / state."""
|
||||
rows = []
|
||||
for severity, window in SLA_HOURS.items():
|
||||
at_risk_at = window * AT_RISK_THRESHOLD
|
||||
rows += [
|
||||
_add(severity, 0), # fresh -> ok
|
||||
_add(severity, at_risk_at * 0.5), # halfway -> ok
|
||||
_add(severity, at_risk_at + 1), # at risk
|
||||
_add(severity, window + 1), # breached
|
||||
# already-notified variants
|
||||
_add(severity, at_risk_at + 1, sla_notified='at_risk'),
|
||||
_add(severity, window + 1, sla_notified='at_risk'),
|
||||
_add(severity, window + 1, sla_notified='breached'),
|
||||
# non-actionable / other statuses
|
||||
_add(severity, window + 1, status='resolved'),
|
||||
_add(severity, window + 1, status='in_progress'),
|
||||
_add(severity, window + 1, status='pending_verification'),
|
||||
]
|
||||
db.session.commit()
|
||||
return rows
|
||||
|
||||
|
||||
def test_prefilter_never_misses_an_actionable_issue(population):
|
||||
"""Safety property: every row the old loop notified is still selected."""
|
||||
expected = {i.id for i in population if _old_loop_would_act(i)}
|
||||
actual = {i.id for i in _candidate_query().all()}
|
||||
|
||||
assert expected, 'fixture built no actionable issues -- test is vacuous'
|
||||
assert expected <= actual, (
|
||||
'the prefilter drops issues the old loop would have alerted on: '
|
||||
f'{sorted(expected - actual)}'
|
||||
)
|
||||
|
||||
|
||||
def test_prefilter_extras_are_all_skipped_by_the_loop(population):
|
||||
"""The extra rows the SQL lets through produce no extra notifications.
|
||||
|
||||
Each must be a row the Python loop independently skips, so the set of alerts
|
||||
actually sent is unchanged.
|
||||
"""
|
||||
by_id = {i.id: i for i in population}
|
||||
expected = {i.id for i in population if _old_loop_would_act(i)}
|
||||
extras = {i.id for i in _candidate_query().all()} - expected
|
||||
|
||||
for iid in extras:
|
||||
issue = by_id[iid]
|
||||
assert not _old_loop_would_act(issue)
|
||||
# and the only reason it is allowed through is the at_risk bookkeeping
|
||||
assert issue.sla_notified == 'at_risk' and sla_status(issue) == 'at_risk', (
|
||||
f'issue {iid} is an unexplained extra: status={issue.status} '
|
||||
f'severity={issue.severity} sla_notified={issue.sla_notified} '
|
||||
f'sla_status={sla_status(issue)}'
|
||||
)
|
||||
|
||||
|
||||
def test_prefilter_excludes_the_bulk_of_open_issues(population):
|
||||
"""The point of the change: most open issues are never read."""
|
||||
total_open = Issue.query.filter(
|
||||
Issue.status.in_(['open', 'in_progress', 'pending_verification'])
|
||||
).count()
|
||||
candidates = _candidate_query().count()
|
||||
assert candidates < total_open
|
||||
|
||||
|
||||
def test_null_reported_at_is_excluded_not_crashed(app):
|
||||
"""reported_at is nullable and sla_status() raises TypeError on NULL.
|
||||
|
||||
Before the prefilter one such row aborted the whole cron run; it must now be
|
||||
excluded in SQL instead.
|
||||
"""
|
||||
issue = Issue(severity='high', description='x', status='open')
|
||||
db.session.add(issue)
|
||||
db.session.commit()
|
||||
|
||||
# The column carries default=now_eastern, so an ORM insert can never leave
|
||||
# it NULL -- the row has to be forced, which is exactly how a legacy or
|
||||
# raw-SQL-inserted row would look.
|
||||
db.session.execute(
|
||||
db.text('UPDATE issues SET reported_at = NULL WHERE id = :i'),
|
||||
{'i': issue.id},
|
||||
)
|
||||
db.session.commit()
|
||||
db.session.expire(issue)
|
||||
|
||||
assert issue.reported_at is None
|
||||
assert issue.id not in {i.id for i in _candidate_query().all()}
|
||||
with pytest.raises(TypeError):
|
||||
sla_status(issue) # confirms the crash the filter is avoiding
|
||||
Reference in New Issue
Block a user