Skip to content

Commit 3e5d7c1

Browse files
[ENG-11768] Update choices, add new field, check timeout and improve logging (#11814)
* Add developer reminder status and update notification campaign recipient status choices * Handle campaign failure status and log execution time window in send_campaign_batch * Add Sentry logging for campaign retries and execution time window * Improve campaign logging --------- Co-authored-by: Longze Chen <cslzchen@gmail.com>
1 parent 22b2d02 commit 3e5d7c1

3 files changed

Lines changed: 26 additions & 3 deletions

File tree

osf/email/notification_campaign.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from framework.celery_tasks import app as celery_app
66
from celery import chord
77
from django.utils import timezone
8+
from datetime import timedelta
89
from osf.models.notification_campaign import NotificationCampaign, NotificationCampaignRecipient, NotificationCampaignStatus, NotificationCampaignRecipientStatus
910
from osf.email import send_email_with_send_grid
11+
from framework import sentry
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -109,7 +111,9 @@ def process_campaign_retry(*args, **kwargs):
109111
return
110112

111113
if campaign.retries < max_retries:
112-
logger.info(f'Retrying {failed_recipients_count} failed recipients for campaign {campaign_id}')
114+
message = f'[Notification Campaign] Retrying {failed_recipients_count} failed recipients for campaign {campaign_id}'
115+
logger.info(message)
116+
sentry.log_message(message)
113117
filters = {'id__in': failed_recipients.values_list('user_id', flat=True)}
114118
tasks = []
115119
for batch in get_filtered_batches(filters=filters, batch_size=batch_size, campaign_id=campaign_id):
@@ -189,8 +193,20 @@ def send_campaign_batch(context, recipients_ids, notification_type_name='blank',
189193
).first() # TODO cache
190194

191195
if notification_type is None:
196+
if campaign.status != NotificationCampaignStatus.FAILED:
197+
campaign.status = NotificationCampaignStatus.FAILED
198+
campaign.save()
192199
return
193200

201+
execution_time_window = campaign.metadata.get('execution', {}).get('time_window', 8)
202+
if campaign.started_at < timezone.now() - timedelta(hours=execution_time_window):
203+
if not campaign.developer_reminder_sent:
204+
message = f'[Notification Campaign] Campaign {campaign_id} exceeded its execution time window ({execution_time_window}h).'
205+
logger.warning(message)
206+
sentry.log_message(message)
207+
campaign.developer_reminder_sent = True
208+
campaign.save()
209+
194210
recipients_qs = OSFUser.objects.filter(id__in=recipients_ids)
195211
recipient_records = {
196212
'to_create': [],
@@ -243,6 +259,8 @@ def send_campaign_batch(context, recipients_ids, notification_type_name='blank',
243259

244260
except Exception as exc:
245261
logger.error(exc) # TODO update error
262+
sentry.log_exception(exc) # TODO update error
263+
246264
recipient_record.status = NotificationCampaignRecipientStatus.FAILED
247265
recipient_record.error_message = str(exc)
248266
recipient_records[operation].append(recipient_record)

osf/migrations/0045_notificationcampaign_notificationcampaignrecipient_and_more.py renamed to osf/migrations/0045_project_enter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.26 on 2026-07-09 11:38
1+
# Generated by Django 4.2.26 on 2026-07-17 11:20
22

33
from django.conf import settings
44
from django.db import migrations, models
@@ -27,6 +27,7 @@ class Migration(migrations.Migration):
2727
('sent_count', models.PositiveIntegerField(default=0)),
2828
('failed_count', models.PositiveIntegerField(default=0)),
2929
('retries', models.PositiveIntegerField(default=0)),
30+
('developer_reminder_sent', models.BooleanField(default=False)),
3031
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
3132
('notification_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='osf.notificationtype')),
3233
],
@@ -36,7 +37,7 @@ class Migration(migrations.Migration):
3637
fields=[
3738
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
3839
('updated_at', models.DateTimeField(auto_now=True)),
39-
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed')], db_index=True, default='pending', max_length=20)),
40+
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed'), ('skipped', 'Skipped'), ('postponed', 'Postponed')], db_index=True, default='pending', max_length=20)),
4041
('error_message', models.TextField(blank=True, null=True)),
4142
('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='osf.notificationcampaign')),
4243
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),

osf/models/notification_campaign.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class NotificationCampaignRecipientStatus(models.TextChoices):
1515
PENDING = 'pending', 'Pending'
1616
SENT = 'sent', 'Sent'
1717
FAILED = 'failed', 'Failed'
18+
SKIPPED = 'skipped', 'Skipped'
19+
POSTPONED = 'postponed', 'Postponed'
1820

1921

2022
class NotificationCampaign(models.Model):
@@ -64,6 +66,8 @@ class NotificationCampaign(models.Model):
6466
failed_count = models.PositiveIntegerField(default=0)
6567
retries = models.PositiveIntegerField(default=0)
6668

69+
developer_reminder_sent = models.BooleanField(default=False)
70+
6771
def start(self, restart_failed=False):
6872
from osf.email.notification_campaign import start_notification_campaign
6973
self.status = NotificationCampaignStatus.RUNNING

0 commit comments

Comments
 (0)