Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User notifications bundle #4457

Merged
merged 32 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0dcad9f
Update user has notification
Ferril Jun 4, 2024
872990c
Add user notification bundle model
Ferril Jun 4, 2024
2ab8315
Add bundling for user notifications
Ferril Jun 4, 2024
63bc4d0
Fix update metrics
Ferril Jun 4, 2024
7e889fe
Update sending and tracking bundled notifications
Ferril Jun 7, 2024
59a9664
remove commented code
Ferril Jun 7, 2024
94c66ee
Update comment
Ferril Jun 7, 2024
271c99f
Fix updating metrics on user notification
Ferril Jun 10, 2024
ea06498
Fix updating metrics on user notification
Ferril Jun 13, 2024
cb71560
Update user notification bundle
Ferril Jun 18, 2024
b0db509
Add test for updating metrics for bundled notifications
Ferril Jun 18, 2024
c943841
Add tests for bundling notifications
Ferril Jun 18, 2024
0b036e6
Add test for notification bundle
Ferril Jun 18, 2024
1145755
Update send_bundled_notification task
Ferril Jun 19, 2024
4ff84fe
update test
Ferril Jun 19, 2024
59d9d65
Update UserNotificationBundle
Ferril Jun 20, 2024
5d34609
Rename some functions, add comments
Ferril Jun 20, 2024
a44af00
Add `alert_receive_channel` field to `BundledNotification`
Ferril Jul 3, 2024
ef3eecc
Update notification bundle
Ferril Jul 3, 2024
5fc5d5e
Fix notification bundle
Ferril Jul 3, 2024
834f8a2
Fix test
Ferril Jul 3, 2024
0b8a91e
fix sending signal for notification bundle
Ferril Jul 4, 2024
95bb29d
Merge branch 'dev' into user-notification-bundle
Ferril Jul 8, 2024
e85776b
Merge branch 'dev' into user-notification-bundle
Ferril Jul 8, 2024
e0e237d
remove comment
Ferril Jul 8, 2024
e2f2727
Update typing
Ferril Jul 9, 2024
cc1ede0
remove todo comment
Ferril Jul 9, 2024
d6b9242
Merge branch 'dev' into user-notification-bundle
Ferril Jul 10, 2024
0394ea1
Fix notify_user_task
Ferril Jul 10, 2024
4eed7d8
Fix mypy
Ferril Jul 11, 2024
58cf5ef
Merge branch 'dev' into user-notification-bundle
Ferril Jul 16, 2024
1431674
Fix migration
Ferril Jul 16, 2024
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
2 changes: 2 additions & 0 deletions engine/apps/alerts/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class ActionSource(IntegerChoices):

NEXT_ESCALATION_DELAY = 5

BUNDLED_NOTIFICATION_DELAY_SECONDS = 60 * 2 # 2 min


# AlertGroup states verbal
class AlertGroupState(str, Enum):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.2.10 on 2024-06-20 11:00

import apps.base.models.user_notification_policy
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('base', '0005_drop_unused_dynamic_settings'),
('user_management', '0022_alter_team_unique_together'),
('alerts', '0051_remove_escalationpolicy_custom_button_trigger'),
]

operations = [
migrations.CreateModel(
name='UserNotificationBundle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('important', models.BooleanField()),
('notification_channel', models.PositiveSmallIntegerField(default=None, null=True, validators=[apps.base.models.user_notification_policy.validate_channel_choice])),
('last_notified_at', models.DateTimeField(default=None, null=True)),
('notification_task_id', models.CharField(default=None, max_length=100, null=True)),
('eta', models.DateTimeField(default=None, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_bundles', to='user_management.user')),
],
),
migrations.CreateModel(
name='BundledNotification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('bundle_uuid', models.CharField(db_index=True, default=None, max_length=100, null=True)),
('alert_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='alerts.alertgroup')),
('alert_receive_channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='alerts.alertreceivechannel')),
('notification_bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='alerts.usernotificationbundle')),
('notification_policy', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.usernotificationpolicy')),
],
),
migrations.AddConstraint(
model_name='usernotificationbundle',
constraint=models.UniqueConstraint(fields=('user', 'important', 'notification_channel'), name='unique_user_notification_bundle'),
),
]
1 change: 1 addition & 0 deletions engine/apps/alerts/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
from .maintainable_object import MaintainableObject # noqa: F401
from .resolution_note import ResolutionNote, ResolutionNoteSlackMessage # noqa: F401
from .user_has_notification import UserHasNotification # noqa: F401
from .user_notification_bundle import BundledNotification, UserNotificationBundle # noqa: F401
8 changes: 8 additions & 0 deletions engine/apps/alerts/models/user_has_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ class UserHasNotification(models.Model):

class Meta:
unique_together = ("user", "alert_group")

def update_active_task_id(self, task_id):
"""
`active_notification_policy_id` keeps celery task_id of the next scheduled `notify_user_task`
for the current user
"""
self.active_notification_policy_id = task_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we store task_id in active_notification_policy_id field? Please rename or at least add the comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a part of the old model UserHasNotification and wasn't added or changed in this PR. I just moved these lines in separate method to make notify_user_task more readable. Makes sense to add the comment though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe out of scope for this PR but I think this model should be renamed to something like UserActiveNotificationPolicy (UserHasNotification isn't very informative imo 😬 )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree 💯 I'd propose to do it with notify_user_task refactoring (#4629)

self.save(update_fields=["active_notification_policy_id"])
72 changes: 72 additions & 0 deletions engine/apps/alerts/models/user_notification_bundle.py
Ferril marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import datetime

from django.db import models
from django.utils import timezone

from apps.alerts.constants import BUNDLED_NOTIFICATION_DELAY_SECONDS
from apps.base.models import UserNotificationPolicy
from apps.base.models.user_notification_policy import validate_channel_choice


class UserNotificationBundle(models.Model):
NOTIFICATION_CHANNELS_TO_BUNDLE = [
UserNotificationPolicy.NotificationChannel.SMS,
]

user = models.ForeignKey("user_management.User", on_delete=models.CASCADE, related_name="notification_bundles")
important = models.BooleanField()
notification_channel = models.PositiveSmallIntegerField(
validators=[validate_channel_choice], null=True, default=None
)
last_notified_at = models.DateTimeField(default=None, null=True)
notification_task_id = models.CharField(max_length=100, null=True, default=None)
# estimated time of arrival for scheduled send_bundled_notification task
eta = models.DateTimeField(default=None, null=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "important", "notification_channel"], name="unique_user_notification_bundle"
)
]

def notified_recently(self) -> bool:
return (
timezone.now() - self.last_notified_at < timezone.timedelta(seconds=BUNDLED_NOTIFICATION_DELAY_SECONDS)
if self.last_notified_at
else False
)

def eta_is_valid(self) -> bool:
"""
`eta` shows eta of scheduled send_bundled_notification task and should never be less than the current time
(with a 1 minute buffer provided).
`eta` is None means that there is no scheduled task.
"""
if not self.eta or self.eta + timezone.timedelta(minutes=1) >= timezone.now():
return True
return False

def get_notification_eta(self) -> datetime.datetime:
last_notified = self.last_notified_at if self.last_notified_at else timezone.now()
return last_notified + timezone.timedelta(seconds=BUNDLED_NOTIFICATION_DELAY_SECONDS)

def append_notification(self, alert_group, notification_policy):
self.notifications.create(
alert_group=alert_group, notification_policy=notification_policy, alert_receive_channel=alert_group.channel
)

@classmethod
def notification_is_bundleable(cls, notification_channel):
return notification_channel in cls.NOTIFICATION_CHANNELS_TO_BUNDLE
Ferril marked this conversation as resolved.
Show resolved Hide resolved


class BundledNotification(models.Model):
alert_group = models.ForeignKey("alerts.AlertGroup", on_delete=models.CASCADE)
alert_receive_channel = models.ForeignKey("alerts.AlertReceiveChannel", on_delete=models.CASCADE)
notification_policy = models.ForeignKey("base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True)
notification_bundle = models.ForeignKey(
UserNotificationBundle, on_delete=models.CASCADE, related_name="notifications"
)
created_at = models.DateTimeField(auto_now_add=True)
bundle_uuid = models.CharField(max_length=100, null=True, default=None, db_index=True)
Copy link
Contributor

@joeyorlando joeyorlando Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is primarly used right now in as the second arg to PhoneBackend.notify_by_sms_bundle_async, but can we just use pk instead of needing also bundle_uuid, or is there a need for a UUID?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that bundle_uuid groups a bunch of BundledNotifications, that were processed in the send_bundled_notification task (if user got flooded, it can be a hundreds of them). While we are processing these notifications, new notifications can be created and attached to the current UserNotificationBundle and they will have their own bundle_uuid and will be processed and sent in a separate task.
That means, that we can't use UserNotificationBundle pk, because there could be processed and unprocessed notifications attached at the same time, and we want to keep them separately. We don't want to use BundledNotification pks, because there could be a long list of them

3 changes: 1 addition & 2 deletions engine/apps/alerts/paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ def unpage_user(alert_group: AlertGroup, user: User, from_user: User) -> None:
user_has_notification = UserHasNotification.objects.filter(
user=user, alert_group=alert_group
).select_for_update()[0]
user_has_notification.active_notification_policy_id = None
user_has_notification.save(update_fields=["active_notification_policy_id"])
user_has_notification.update_active_task_id(task_id=None)
except IndexError:
return
finally:
Expand Down
Loading
Loading