forked from GithubBackups/healthchecks
Fix sendalerts to clear Profile.next_nag_date if all checks up
Profile.next_nag_date tracks when the next hourly/daily reminder should be sent. Normally, sendalerts sets this field when a check goes down, and sendreports clears it out whenever it is about to send a reminder but realizes all checks are up. The problem: sendalerts can set next_nag_date to a non-null value, but it does not clear it out when all checks are up. This can result in a hourly/daily reminder being sent out at the wrong time. Specific example, assuming hourly reminders: 13:00: Check A goes down. next_nag_date gets set to 14:00. 13:05: Check A goes up. next_nag_date remains set to 14:00. 13:55: Check B goes down. next_nag_date remains set to 14:00. 14:00: Healthchecks sends a hourly reminder, just 5 minutes after Check B going down. It should have sent the reminder at 13:55 + 1 hour = 14:55 The fix: sendalerts can now both set and clear the next_nag_date field. The main changes are in Project.update_next_nag_dates() and in Profile.update_next_nag_date(). With the fix: 13:00: Check A goes down. next_nag_date gets set to 14:00. 13:05: Check A goes up. next_nag_date gets set to null. 13:55: Check B goes down. next_nag_date gets set to 14:55. 14:55: Healthchecks sends a hourly reminder.
This commit is contained in:
parent
502ff7567e
commit
7ba5fcbb71
@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
- Fix downtime summary to handle months when the check didn't exist yet (#472)
|
- Fix downtime summary to handle months when the check didn't exist yet (#472)
|
||||||
- Relax cron expression validation: accept all expressions that croniter accepts
|
- Relax cron expression validation: accept all expressions that croniter accepts
|
||||||
|
- Fix sendalerts to clear Profile.next_nag_date if all checks up
|
||||||
|
|
||||||
## v1.19.0 - 2021-02-03
|
## v1.19.0 - 2021-02-03
|
||||||
|
|
||||||
|
@ -135,8 +135,8 @@ class Profile(models.Model):
|
|||||||
def projects(self):
|
def projects(self):
|
||||||
""" Return a queryset of all projects we have access to. """
|
""" Return a queryset of all projects we have access to. """
|
||||||
|
|
||||||
is_owner = Q(owner=self.user)
|
is_owner = Q(owner_id=self.user_id)
|
||||||
is_member = Q(member__user=self.user)
|
is_member = Q(member__user_id=self.user_id)
|
||||||
q = Project.objects.filter(is_owner | is_member)
|
q = Project.objects.filter(is_owner | is_member)
|
||||||
return q.distinct().order_by("name")
|
return q.distinct().order_by("name")
|
||||||
|
|
||||||
@ -267,6 +267,15 @@ class Profile(models.Model):
|
|||||||
def can_accept(self, project):
|
def can_accept(self, project):
|
||||||
return project.num_checks() <= self.num_checks_available()
|
return project.num_checks() <= self.num_checks_available()
|
||||||
|
|
||||||
|
def update_next_nag_date(self):
|
||||||
|
any_down = self.checks_from_all_projects().filter(status="down").exists()
|
||||||
|
if any_down and self.next_nag_date is None and self.nag_period:
|
||||||
|
self.next_nag_date = timezone.now() + self.nag_period
|
||||||
|
self.save(update_fields=["next_nag_date"])
|
||||||
|
elif not any_down and self.next_nag_date:
|
||||||
|
self.next_nag_date = None
|
||||||
|
self.save(update_fields=["next_nag_date"])
|
||||||
|
|
||||||
|
|
||||||
class Project(models.Model):
|
class Project(models.Model):
|
||||||
code = models.UUIDField(default=uuid.uuid4, unique=True)
|
code = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
@ -319,17 +328,15 @@ class Project(models.Model):
|
|||||||
user.profile.send_instant_login_link(self, redirect_url=checks_url)
|
user.profile.send_instant_login_link(self, redirect_url=checks_url)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_next_nag_date(self):
|
def update_next_nag_dates(self):
|
||||||
""" Set next_nag_date on profiles of all members of this project. """
|
""" Update next_nag_date on profiles of all members of this project. """
|
||||||
|
|
||||||
is_owner = Q(user=self.owner)
|
is_owner = Q(user_id=self.owner_id)
|
||||||
is_member = Q(user__memberships__project=self)
|
is_member = Q(user__memberships__project=self)
|
||||||
q = Profile.objects.filter(is_owner | is_member)
|
q = Profile.objects.filter(is_owner | is_member).exclude(nag_period=NO_NAG)
|
||||||
q = q.exclude(nag_period=NO_NAG)
|
|
||||||
# Exclude profiles with next_nag_date already set
|
|
||||||
q = q.filter(next_nag_date__isnull=True)
|
|
||||||
|
|
||||||
q.update(next_nag_date=timezone.now() + models.F("nag_period"))
|
for profile in q:
|
||||||
|
profile.update_next_nag_date()
|
||||||
|
|
||||||
def overall_status(self):
|
def overall_status(self):
|
||||||
status = "up"
|
status = "up"
|
||||||
|
36
hc/accounts/tests/test_profile_model.py
Normal file
36
hc/accounts/tests/test_profile_model.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
from hc.api.models import Check
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileModelTestCase(BaseTestCase):
|
||||||
|
def test_it_sets_next_nag_date(self):
|
||||||
|
Check.objects.create(project=self.project, status="down")
|
||||||
|
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.update_next_nag_date()
|
||||||
|
|
||||||
|
self.assertTrue(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
def test_it_does_not_set_next_nag_date_if_no_nag_period(self):
|
||||||
|
Check.objects.create(project=self.project, status="down")
|
||||||
|
self.profile.update_next_nag_date()
|
||||||
|
self.assertIsNone(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
def test_it_does_not_update_existing_next_nag_date(self):
|
||||||
|
Check.objects.create(project=self.project, status="down")
|
||||||
|
|
||||||
|
original_nag_date = now() - td(minutes=30)
|
||||||
|
|
||||||
|
self.profile.next_nag_date = original_nag_date
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.update_next_nag_date()
|
||||||
|
|
||||||
|
self.assertEqual(self.profile.next_nag_date, original_nag_date)
|
||||||
|
|
||||||
|
def test_it_clears_next_nag_date(self):
|
||||||
|
self.profile.next_nag_date = now()
|
||||||
|
self.profile.update_next_nag_date()
|
||||||
|
self.assertIsNone(self.profile.next_nag_date)
|
@ -23,9 +23,8 @@ def notify(flip_id, stdout):
|
|||||||
|
|
||||||
stdout.write(SENDING_TMPL % (flip.new_status, check.code))
|
stdout.write(SENDING_TMPL % (flip.new_status, check.code))
|
||||||
|
|
||||||
# Set dates for followup nags
|
# Set or clear dates for followup nags
|
||||||
if flip.new_status == "down":
|
check.project.update_next_nag_dates()
|
||||||
check.project.set_next_nag_date()
|
|
||||||
|
|
||||||
# Send notifications
|
# Send notifications
|
||||||
send_start = timezone.now()
|
send_start = timezone.now()
|
||||||
|
@ -101,25 +101,10 @@ class SendAlertsTestCase(BaseTestCase):
|
|||||||
# It should call `notify` instead of `notify_on_thread`
|
# It should call `notify` instead of `notify_on_thread`
|
||||||
self.assertTrue(mock_notify.called)
|
self.assertTrue(mock_notify.called)
|
||||||
|
|
||||||
def test_it_updates_owners_next_nag_date(self):
|
def test_it_sets_next_nag_date(self):
|
||||||
self.profile.nag_period = td(hours=1)
|
self.profile.nag_period = td(hours=1)
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
check = Check(project=self.project, status="down")
|
|
||||||
check.last_ping = now() - td(days=2)
|
|
||||||
check.save()
|
|
||||||
|
|
||||||
flip = Flip(owner=check, created=check.last_ping)
|
|
||||||
flip.old_status = "up"
|
|
||||||
flip.new_status = "down"
|
|
||||||
flip.save()
|
|
||||||
|
|
||||||
notify(flip.id, Mock())
|
|
||||||
|
|
||||||
self.profile.refresh_from_db()
|
|
||||||
self.assertIsNotNone(self.profile.next_nag_date)
|
|
||||||
|
|
||||||
def test_it_updates_members_next_nag_date(self):
|
|
||||||
self.bobs_profile.nag_period = td(hours=1)
|
self.bobs_profile.nag_period = td(hours=1)
|
||||||
self.bobs_profile.save()
|
self.bobs_profile.save()
|
||||||
|
|
||||||
@ -134,9 +119,42 @@ class SendAlertsTestCase(BaseTestCase):
|
|||||||
|
|
||||||
notify(flip.id, Mock())
|
notify(flip.id, Mock())
|
||||||
|
|
||||||
|
# next_nag_gate should now be set for the project's owner
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
# next_nag_gate should now be set for the project's members
|
||||||
self.bobs_profile.refresh_from_db()
|
self.bobs_profile.refresh_from_db()
|
||||||
self.assertIsNotNone(self.bobs_profile.next_nag_date)
|
self.assertIsNotNone(self.bobs_profile.next_nag_date)
|
||||||
|
|
||||||
|
def test_it_clears_next_nag_date(self):
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.next_nag_date = now() - td(minutes=30)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
self.bobs_profile.nag_period = td(hours=1)
|
||||||
|
self.bobs_profile.next_nag_date = now() - td(minutes=30)
|
||||||
|
self.bobs_profile.save()
|
||||||
|
|
||||||
|
check = Check(project=self.project, status="up")
|
||||||
|
check.last_ping = now()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
flip = Flip(owner=check, created=check.last_ping)
|
||||||
|
flip.old_status = "down"
|
||||||
|
flip.new_status = "up"
|
||||||
|
flip.save()
|
||||||
|
|
||||||
|
notify(flip.id, Mock())
|
||||||
|
|
||||||
|
# next_nag_gate should now be cleared out for the project's owner
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertIsNone(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
# next_nag_gate should now be cleared out for the project's members
|
||||||
|
self.bobs_profile.refresh_from_db()
|
||||||
|
self.assertIsNone(self.bobs_profile.next_nag_date)
|
||||||
|
|
||||||
def test_it_does_not_touch_already_set_next_nag_dates(self):
|
def test_it_does_not_touch_already_set_next_nag_dates(self):
|
||||||
original_nag_date = now() - td(minutes=30)
|
original_nag_date = now() - td(minutes=30)
|
||||||
self.profile.nag_period = td(hours=1)
|
self.profile.nag_period = td(hours=1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user