chore(): small improvements
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-23 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0002_alter_notificationchannel_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='notificationrule',
|
||||
name='rate_limit_max_notifications',
|
||||
field=models.PositiveIntegerField(default=1, verbose_name='rate limit max notifications'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notificationrule',
|
||||
name='rate_limit_window_seconds',
|
||||
field=models.PositiveIntegerField(default=60, verbose_name='rate limit window seconds'),
|
||||
),
|
||||
]
|
||||
@@ -93,6 +93,14 @@ class NotificationRule(BaseModel):
|
||||
default=True,
|
||||
verbose_name=_("is active"),
|
||||
)
|
||||
rate_limit_window_seconds = models.PositiveIntegerField(
|
||||
default=60,
|
||||
verbose_name=_("rate limit window seconds"),
|
||||
)
|
||||
rate_limit_max_notifications = models.PositiveIntegerField(
|
||||
default=1,
|
||||
verbose_name=_("rate limit max notifications"),
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("created at"),
|
||||
|
||||
@@ -82,11 +82,15 @@ def rule_create(
|
||||
event_type: str,
|
||||
channel: NotificationChannel,
|
||||
experiment: Any | None = None,
|
||||
rate_limit_window_seconds: int = 60,
|
||||
rate_limit_max_notifications: int = 1,
|
||||
) -> NotificationRule:
|
||||
rule = NotificationRule(
|
||||
event_type=event_type,
|
||||
channel=channel,
|
||||
experiment=experiment,
|
||||
rate_limit_window_seconds=rate_limit_window_seconds,
|
||||
rate_limit_max_notifications=rate_limit_max_notifications,
|
||||
)
|
||||
rule.save()
|
||||
return rule
|
||||
@@ -97,7 +101,12 @@ def rule_update(
|
||||
rule: NotificationRule,
|
||||
**fields: Any,
|
||||
) -> NotificationRule:
|
||||
allowed = {"event_type", "is_active"}
|
||||
allowed = {
|
||||
"event_type",
|
||||
"is_active",
|
||||
"rate_limit_window_seconds",
|
||||
"rate_limit_max_notifications",
|
||||
}
|
||||
for key in fields:
|
||||
if key not in allowed:
|
||||
raise ValueError(f"Field '{key}' cannot be updated.")
|
||||
@@ -149,12 +158,17 @@ def notification_enqueue(
|
||||
|
||||
logs: list[NotificationLog] = []
|
||||
for rule in rules:
|
||||
event_key = _build_event_key(event_type, payload)
|
||||
if NotificationLog.objects.filter(
|
||||
event_key = _build_event_key(
|
||||
event_type,
|
||||
payload,
|
||||
rule.rate_limit_window_seconds,
|
||||
)
|
||||
sent_or_pending = NotificationLog.objects.filter(
|
||||
event_key=event_key,
|
||||
channel=rule.channel,
|
||||
status__in=[NotificationStatus.PENDING, NotificationStatus.SENT],
|
||||
).exists():
|
||||
).count()
|
||||
if sent_or_pending >= rule.rate_limit_max_notifications:
|
||||
continue
|
||||
|
||||
log = NotificationLog.objects.create(
|
||||
@@ -176,8 +190,13 @@ def notification_enqueue(
|
||||
return logs
|
||||
|
||||
|
||||
def _build_event_key(event_type: str, payload: NotificationPayload) -> str:
|
||||
bucket = int(timezone.now().timestamp()) // 60
|
||||
def _build_event_key(
|
||||
event_type: str,
|
||||
payload: NotificationPayload,
|
||||
window_seconds: int,
|
||||
) -> str:
|
||||
normalized_window = max(window_seconds, 1)
|
||||
bucket = int(timezone.now().timestamp()) // normalized_window
|
||||
return f"{event_type}:{payload.experiment_id}:{bucket}"
|
||||
|
||||
|
||||
|
||||
@@ -164,6 +164,33 @@ class NotificationEnqueueTest(TestCase):
|
||||
self.assertEqual(len(logs_1), 1)
|
||||
self.assertEqual(len(logs_2), 0)
|
||||
|
||||
def test_enqueue_respects_rule_rate_limit(self) -> None:
|
||||
rule_create(
|
||||
event_type=NotificationEventType.GUARDRAIL_TRIGGERED,
|
||||
channel=self.channel,
|
||||
rate_limit_window_seconds=60,
|
||||
rate_limit_max_notifications=2,
|
||||
)
|
||||
payload = NotificationPayload(
|
||||
title="Alert",
|
||||
body="Error rate exceeded",
|
||||
event_type=NotificationEventType.GUARDRAIL_TRIGGERED,
|
||||
experiment_id=str(self.experiment.pk),
|
||||
experiment_name=self.experiment.name,
|
||||
)
|
||||
logs_1 = notification_enqueue(
|
||||
NotificationEventType.GUARDRAIL_TRIGGERED, payload
|
||||
)
|
||||
logs_2 = notification_enqueue(
|
||||
NotificationEventType.GUARDRAIL_TRIGGERED, payload
|
||||
)
|
||||
logs_3 = notification_enqueue(
|
||||
NotificationEventType.GUARDRAIL_TRIGGERED, payload
|
||||
)
|
||||
self.assertEqual(len(logs_1), 1)
|
||||
self.assertEqual(len(logs_2), 1)
|
||||
self.assertEqual(len(logs_3), 0)
|
||||
|
||||
def test_enqueue_no_matching_rules(self) -> None:
|
||||
logs = notification_enqueue(
|
||||
NotificationEventType.EXPERIMENT_STARTED,
|
||||
|
||||
Reference in New Issue
Block a user