oncall-engine/engine/apps/alerts/tests/test_acknowledge_reminder.py
Yulya Artyukhina 8420cfd822
Fix acknowledge reminder task (#5179)
# What this PR does
- Adds 10 minutes lock for acknowledge reminder task to prevent task
duplicates, that causes posting multiple reminder messages and flooding
in Slack threads.
- Adds a new signal for acknowledge reminder task instead of using
`alert_group_action_triggered_signal` since it is used only to post
reminder message in Slack thread and it's not needed to be processed by
other representatives

## Which issue(s) this PR closes

Related to https://github.com/grafana/oncall-private/issues/2953

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-10-16 12:13:28 +00:00

418 lines
16 KiB
Python

from datetime import timedelta
from unittest.mock import patch
import pytest
from celery import uuid as celery_uuid
from django.core.cache import cache
from django.utils import timezone
from apps.alerts.constants import ActionSource
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
from apps.alerts.tasks import acknowledge_reminder_task
from apps.alerts.tasks.acknowledge_reminder import send_post_ack_reminder_message_signal, unacknowledge_timeout_task
from apps.user_management.models import Organization
TASK_ID = "TASK_ID"
def _parametrize_or(best, worst):
"""
Utility method to parametrize tests with multiple OR conditions. best = best case, when all the conditions in
the OR statement are True. worst = worst case, when all the conditions in the OR statement are False.
"""
assert len(best) == len(worst)
return [(*best[:i], worst[i], *best[i + 1 :]) for i in range(len(best))]
@pytest.fixture
def ack_reminder_test_setup(
make_organization,
make_user,
make_alert_receive_channel,
make_alert_group,
make_alert,
):
def _ack_reminder_test_setup(
task_id=TASK_ID,
acknowledged=True,
acknowledged_by=AlertGroup.USER,
resolved=False,
set_root_alert_group=False,
acknowledge_remind_timeout=Organization.ACKNOWLEDGE_REMIND_1H,
unacknowledge_timeout=Organization.UNACKNOWLEDGE_TIMEOUT_5MIN,
acknowledged_by_confirmed=None,
):
organization = make_organization(
acknowledge_remind_timeout=acknowledge_remind_timeout, unacknowledge_timeout=unacknowledge_timeout
)
user = make_user(organization=organization)
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel=alert_receive_channel)
alert_group = make_alert_group(
alert_receive_channel,
acknowledged=acknowledged,
acknowledged_by=acknowledged_by,
acknowledged_by_user=user,
resolved=resolved,
root_alert_group_id=root_alert_group.id if set_root_alert_group else None,
)
alert_group.last_unique_unacknowledge_process_id = task_id
alert_group.acknowledged_by_confirmed = acknowledged_by_confirmed
alert_group.save(update_fields=["last_unique_unacknowledge_process_id", "acknowledged_by_confirmed"])
return organization, alert_group, user
return _ack_reminder_test_setup
@pytest.mark.django_db
def test_acknowledge_by_user_invokes_start_ack_reminder(ack_reminder_test_setup):
organization, alert_group, user = ack_reminder_test_setup(acknowledged=False)
with patch.object(alert_group, "start_ack_reminder_if_needed") as mock_start_ack_reminder:
alert_group.acknowledge_by_user_or_backsync(user, ActionSource.SLACK)
mock_start_ack_reminder.assert_called_once_with()
@pytest.mark.django_db
def test_bulk_acknowledge_invokes_start_ack_reminder(ack_reminder_test_setup):
organization, alert_group, user = ack_reminder_test_setup(acknowledged=False)
with patch.object(AlertGroup, "start_ack_reminder_if_needed") as mock_start_ack_reminder:
AlertGroup.bulk_acknowledge(user, AlertGroup.objects.filter(pk=alert_group.pk))
mock_start_ack_reminder.assert_called_once_with()
@pytest.mark.django_db
def test_start_ack_reminder_invokes_acknowledge_reminder_task(ack_reminder_test_setup):
organization, alert_group, user = ack_reminder_test_setup()
# make sure celery_uuid returns a string to be passed to the task
assert type(celery_uuid()) == str
with patch.object(acknowledge_reminder_task, "apply_async") as mock_acknowledge_reminder_task:
with patch("apps.alerts.models.alert_group.celery_uuid", return_value=TASK_ID):
alert_group.start_ack_reminder_if_needed()
mock_acknowledge_reminder_task.assert_called_once_with(
(alert_group.pk, TASK_ID),
countdown=Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout],
)
@pytest.mark.parametrize(
"set_root_alert_group,acknowledge_remind_timeout",
_parametrize_or(
best=(False, Organization.ACKNOWLEDGE_REMIND_1H),
worst=(True, Organization.ACKNOWLEDGE_REMIND_NEVER),
),
)
@pytest.mark.django_db
def test_ack_reminder_skip(ack_reminder_test_setup, set_root_alert_group, acknowledge_remind_timeout):
organization, alert_group, user = ack_reminder_test_setup(
acknowledge_remind_timeout=acknowledge_remind_timeout, set_root_alert_group=set_root_alert_group
)
with patch.object(acknowledge_reminder_task, "apply_async") as mock_acknowledge_reminder_task:
alert_group.start_ack_reminder_if_needed()
mock_acknowledge_reminder_task.assert_not_called()
@pytest.mark.parametrize(
"task_id,acknowledged,acknowledged_by,resolved,set_root_alert_group,acknowledge_remind_timeout",
_parametrize_or(
best=(TASK_ID, True, AlertGroup.USER, False, False, Organization.ACKNOWLEDGE_REMIND_1H),
worst=(None, False, AlertGroup.SOURCE, True, True, Organization.ACKNOWLEDGE_REMIND_NEVER),
),
)
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@pytest.mark.django_db
def test_acknowledge_reminder_task_skip(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
task_id,
acknowledged,
acknowledged_by,
resolved,
set_root_alert_group,
acknowledge_remind_timeout,
):
organization, alert_group, user = ack_reminder_test_setup(
task_id=task_id,
acknowledged=acknowledged,
acknowledged_by=acknowledged_by,
resolved=resolved,
set_root_alert_group=set_root_alert_group,
acknowledge_remind_timeout=acknowledge_remind_timeout,
)
acknowledge_reminder_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert not alert_group.log_records.exists()
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@patch.object(send_post_ack_reminder_message_signal, "delay")
@pytest.mark.django_db
def test_acknowledge_reminder_task_reschedules_itself_and_sends_signal(
mock_send_post_ack_reminder_message_signal,
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
django_capture_on_commit_callbacks,
):
organization, alert_group, user = ack_reminder_test_setup(
unacknowledge_timeout=Organization.UNACKNOWLEDGE_TIMEOUT_NEVER
)
with django_capture_on_commit_callbacks(execute=True) as callbacks:
acknowledge_reminder_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_called_once_with(
(alert_group.pk, TASK_ID),
countdown=Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout],
)
log_record = alert_group.log_records.get()
assert log_record.type == AlertGroupLogRecord.TYPE_ACK_REMINDER_TRIGGERED
assert log_record.author == alert_group.acknowledged_by_user
# send_post_ack_reminder_message_signal task is queued after commit
assert len(callbacks) == 1
mock_send_post_ack_reminder_message_signal.assert_called_once_with(log_record.id)
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@pytest.mark.django_db
def test_acknowledge_reminder_task_invokes_unacknowledge_timeout_task(
mock_acknowledge_reminder_task, mock_unacknowledge_timeout_task, ack_reminder_test_setup
):
organization, alert_group, user = ack_reminder_test_setup(
unacknowledge_timeout=Organization.UNACKNOWLEDGE_TIMEOUT_5MIN
)
acknowledge_reminder_task(alert_group.pk, TASK_ID)
mock_acknowledge_reminder_task.assert_not_called()
mock_unacknowledge_timeout_task.assert_called_with(
(alert_group.pk, TASK_ID),
countdown=Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[organization.unacknowledge_timeout],
)
alert_group.refresh_from_db()
assert alert_group.acknowledged_by_confirmed is None
log_record = alert_group.log_records.get()
assert log_record.type == AlertGroupLogRecord.TYPE_ACK_REMINDER_TRIGGERED
assert log_record.author == alert_group.acknowledged_by_user
@pytest.mark.parametrize(
"task_id,acknowledged,acknowledged_by,resolved,set_root_alert_group,acknowledge_remind_timeout,unacknowledge_timeout",
_parametrize_or(
best=(
TASK_ID,
True,
AlertGroup.USER,
False,
False,
Organization.ACKNOWLEDGE_REMIND_1H,
Organization.UNACKNOWLEDGE_TIMEOUT_5MIN,
),
worst=(
None,
False,
AlertGroup.SOURCE,
True,
True,
Organization.ACKNOWLEDGE_REMIND_NEVER,
Organization.UNACKNOWLEDGE_TIMEOUT_NEVER,
),
),
)
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@pytest.mark.django_db
def test_unacknowledge_timeout_task_skip(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
task_id,
acknowledged,
acknowledged_by,
resolved,
set_root_alert_group,
acknowledge_remind_timeout,
unacknowledge_timeout,
):
organization, alert_group, user = ack_reminder_test_setup(
task_id=task_id,
acknowledged=acknowledged,
acknowledged_by=acknowledged_by,
resolved=resolved,
set_root_alert_group=set_root_alert_group,
acknowledge_remind_timeout=acknowledge_remind_timeout,
unacknowledge_timeout=unacknowledge_timeout,
)
unacknowledge_timeout_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert not alert_group.log_records.exists()
@patch.object(AlertGroup, "start_escalation_if_needed")
@patch.object(AlertGroup, "unacknowledge")
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@pytest.mark.django_db
def test_unacknowledge_timeout_task_unacknowledge(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
mock_unacknowledge,
mock_start_escalation_if_needed,
ack_reminder_test_setup,
):
organization, alert_group, user = ack_reminder_test_setup()
unacknowledge_timeout_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
log_record = alert_group.log_records.get()
assert log_record.type == AlertGroupLogRecord.TYPE_AUTO_UN_ACK
assert log_record.author == alert_group.acknowledged_by_user
mock_unacknowledge.assert_called_once_with()
mock_start_escalation_if_needed.assert_called_once_with()
@patch.object(unacknowledge_timeout_task, "apply_async")
@patch.object(acknowledge_reminder_task, "apply_async")
@pytest.mark.django_db
def test_unacknowledge_timeout_task_no_unacknowledge(
mock_acknowledge_reminder_task, mock_unacknowledge_timeout_task, ack_reminder_test_setup
):
organization, alert_group, user = ack_reminder_test_setup(acknowledged_by_confirmed=timezone.now())
unacknowledge_timeout_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_called_once_with(
(alert_group.pk, TASK_ID),
countdown=Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout]
- Organization.UNACKNOWLEDGE_TIMEOUT_DELAY[organization.unacknowledge_timeout],
)
assert not alert_group.log_records.exists()
@patch.object(acknowledge_reminder_task, "apply_async")
@patch.object(unacknowledge_timeout_task, "apply_async")
@pytest.mark.django_db
def test_ack_reminder_skip_deleted_org(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
):
organization, alert_group, user = ack_reminder_test_setup()
organization.deleted_at = timezone.now()
organization.save()
acknowledge_reminder_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert not alert_group.log_records.exists()
@patch.object(acknowledge_reminder_task, "apply_async")
@patch.object(unacknowledge_timeout_task, "apply_async")
@pytest.mark.django_db
def test_unacknowledge_timeout_task_skip_deleted_org(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
):
organization, alert_group, user = ack_reminder_test_setup()
organization.deleted_at = timezone.now()
organization.save()
unacknowledge_timeout_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert not alert_group.log_records.exists()
@patch.object(acknowledge_reminder_task, "apply_async")
@patch.object(unacknowledge_timeout_task, "apply_async")
@pytest.mark.django_db
def test_ack_reminder_cancel_too_old(
mock_acknowledge_reminder_task,
mock_unacknowledge_timeout_task,
ack_reminder_test_setup,
settings,
):
organization, alert_group, user = ack_reminder_test_setup(
unacknowledge_timeout=Organization.UNACKNOWLEDGE_TIMEOUT_NEVER
)
alert_group.started_at = timezone.now() - timedelta(days=settings.ACKNOWLEDGE_REMINDER_TASK_EXPIRY_DAYS + 1)
alert_group.save()
acknowledge_reminder_task(alert_group.pk, TASK_ID)
mock_unacknowledge_timeout_task.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert not alert_group.log_records.exists()
@pytest.mark.django_db
def test_acknowledge_reminder_skip_doubled_notification(
ack_reminder_test_setup,
django_capture_on_commit_callbacks,
caplog,
):
organization, alert_group, user = ack_reminder_test_setup(
unacknowledge_timeout=Organization.UNACKNOWLEDGE_TIMEOUT_NEVER
)
expected_log_text = f"Acknowledge reminder for alert_group {alert_group.id} has already been sent recently."
# check task lock cache doesn't exist
lock_id = f"acknowledge-reminder-lock-{alert_group.id}"
assert cache.get(lock_id) is None
with patch.object(acknowledge_reminder_task, "apply_async") as mock_acknowledge_reminder_task:
with patch.object(send_post_ack_reminder_message_signal, "delay") as mock_send_signal:
with django_capture_on_commit_callbacks(execute=True):
acknowledge_reminder_task(alert_group.pk, TASK_ID)
log_record = alert_group.log_records.get()
assert log_record.type == AlertGroupLogRecord.TYPE_ACK_REMINDER_TRIGGERED
mock_send_signal.assert_called_once_with(log_record.id)
mock_acknowledge_reminder_task.assert_called_once()
# check task lock cache exists
assert cache.get(lock_id) == TASK_ID
assert expected_log_text not in caplog.text
# acknowledge_reminder_task doesn't proceed the second time if it has been called recently
with patch.object(acknowledge_reminder_task, "apply_async") as mock_acknowledge_reminder_task:
with patch.object(send_post_ack_reminder_message_signal, "delay") as mock_send_signal:
with django_capture_on_commit_callbacks(execute=True):
acknowledge_reminder_task(alert_group.pk, TASK_ID)
assert alert_group.log_records.count() == 1
mock_send_signal.assert_not_called()
mock_acknowledge_reminder_task.assert_not_called()
assert expected_log_text in caplog.text