Skip to content

Commit b2277f0

Browse files
committed
Change how we use the send pending email task
1 parent 90be065 commit b2277f0

File tree

5 files changed

+67
-112
lines changed

5 files changed

+67
-112
lines changed

backend/notifications/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def send_email(
192192
if not recipient and not recipient_email:
193193
raise ValueError("Either recipient or recipient_email must be provided")
194194

195-
from notifications.tasks import send_pending_emails
195+
from notifications.tasks import send_pending_email
196196

197197
recipient_email = recipient_email or recipient.email
198198

@@ -207,7 +207,7 @@ def send_email(
207207
or settings.DEFAULT_FROM_EMAIL
208208
)
209209

210-
SentEmail.objects.create(
210+
sent_email = SentEmail.objects.create(
211211
email_template=self,
212212
conference=self.conference,
213213
from_email=from_email,
@@ -223,7 +223,7 @@ def send_email(
223223
bcc_addresses=self.bcc_addresses,
224224
)
225225

226-
transaction.on_commit(lambda: send_pending_emails.delay())
226+
transaction.on_commit(lambda: send_pending_email.delay(sent_email.id))
227227

228228
@property
229229
def is_custom(self):

backend/notifications/tasks.py

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,45 @@
1010
logger = logging.getLogger(__name__)
1111

1212

13-
@app.task(base=OnlyOneAtTimeTask)
14-
def send_pending_emails():
15-
pending_emails = (
16-
SentEmail.objects.pending().order_by("created").values_list("id", flat=True)
13+
@app.task(
14+
base=OnlyOneAtTimeTask,
15+
bind=True,
16+
autoretry_for=(Exception,),
17+
retry_backoff=5,
18+
max_retries=5,
19+
)
20+
@transaction.atomic()
21+
def send_pending_email(self, sent_email_id: int):
22+
email_backend_connection = get_connection()
23+
24+
sent_email = (
25+
SentEmail.objects.select_for_update(skip_locked=True)
26+
.pending()
27+
.filter(id=sent_email_id)
28+
.first()
1729
)
18-
total_pending_emails = pending_emails.count()
1930

20-
if total_pending_emails == 0:
31+
if not sent_email:
2132
return
2233

23-
logger.info("Found %s pending emails", pending_emails.count())
24-
25-
email_backend_connection = get_connection()
26-
27-
for email_id in pending_emails.iterator():
28-
with transaction.atomic():
29-
sent_email = (
30-
SentEmail.objects.select_for_update(skip_locked=True)
31-
.filter(
32-
id=email_id,
33-
)
34-
.first()
35-
)
34+
if self.request.retries >= self.max_retries - 1:
35+
sent_email.mark_as_failed()
36+
logger.error(
37+
"Failed to send email sent_email_id=%s",
38+
sent_email.id,
39+
)
40+
return
3641

37-
if not sent_email or not sent_email.is_pending:
38-
return
42+
logger.info("Sending sent_email=%s", sent_email.id)
3943

40-
try:
41-
message_id = send_email(sent_email, email_backend_connection)
42-
sent_email.mark_as_sent(message_id)
44+
message_id = send_email(sent_email, email_backend_connection)
45+
sent_email.mark_as_sent(message_id)
4346

44-
logger.info(
45-
"Email sent_email_id=%s sent with message_id=%s",
46-
sent_email.id,
47-
message_id,
48-
)
49-
except Exception as e:
50-
sent_email.mark_as_failed()
51-
logger.exception(
52-
"Failed to send email sent_email_id=%s error=%s",
53-
email_id,
54-
e,
55-
exc_info=e,
56-
)
47+
logger.info(
48+
"Email sent_email_id=%s sent with message_id=%s",
49+
sent_email.id,
50+
message_id,
51+
)
5752

5853

5954
def send_email(sent_email, email_backend_connection):

backend/notifications/tests/test_models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ def test_send_email_template_to_recipient_email(
5252
reply_to="[email protected]",
5353
)
5454

55-
mock_send_pending_emails = mocker.patch(
56-
"notifications.tasks.send_pending_emails.delay"
55+
mock_send_pending_email = mocker.patch(
56+
"notifications.tasks.send_pending_email.delay"
5757
)
5858

5959
with django_capture_on_commit_callbacks(execute=True):
@@ -64,12 +64,12 @@ def test_send_email_template_to_recipient_email(
6464
},
6565
)
6666

67-
mock_send_pending_emails.assert_called_once()
68-
6967
sent_email = SentEmail.objects.get(
7068
email_template=email_template,
7169
)
7270

71+
mock_send_pending_email.assert_called_once_with(sent_email.id)
72+
7373
assert sent_email.recipient is None
7474
assert sent_email.recipient_email == "[email protected]"
7575

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1+
from unittest import mock
12
from unittest.mock import patch
23
import time_machine
34
from django.core import mail
4-
from notifications.tasks import send_pending_emails
5+
from notifications.tasks import send_pending_email
56
from notifications.models import SentEmail
67
from notifications.tests.factories import SentEmailFactory
78

89

9-
def test_send_pending_emails_does_nothing_with_no_pending_emails():
10-
SentEmailFactory(status=SentEmail.Status.sent, sent_at="2020-01-01 12:00Z")
11-
SentEmailFactory(status=SentEmail.Status.sent, sent_at="2020-01-01 12:00Z")
12-
SentEmailFactory(status=SentEmail.Status.sent, sent_at="2020-01-01 12:00Z")
13-
SentEmailFactory(status=SentEmail.Status.sent, sent_at="2020-01-01 12:00Z")
10+
def test_send_pending_email_does_nothing_with_non_pending_email():
11+
sent_email = SentEmailFactory(
12+
status=SentEmail.Status.sent, sent_at="2020-01-01 12:00Z"
13+
)
1414

15-
send_pending_emails()
15+
send_pending_email(sent_email.id)
1616

1717
assert len(mail.outbox) == 0
1818

1919

20-
def test_send_pending_emails_task_sends_data_correctly():
20+
def test_send_pending_email_task_sends_data_correctly():
2121
pending_email_1 = SentEmailFactory(
2222
status=SentEmail.Status.pending,
2323
reply_to="[email protected]",
@@ -29,7 +29,8 @@ def test_send_pending_emails_task_sends_data_correctly():
2929
subject="subject",
3030
)
3131

32-
send_pending_emails()
32+
with time_machine.travel("2021-01-01 12:00Z", tick=False):
33+
send_pending_email(pending_email_1.id)
3334

3435
assert len(mail.outbox) == 1
3536

@@ -42,8 +43,16 @@ def test_send_pending_emails_task_sends_data_correctly():
4243
assert mail.outbox[0].alternatives == [("html body", "text/html")]
4344
assert mail.outbox[0].subject == "subject"
4445

46+
pending_email_1.refresh_from_db()
47+
48+
assert len(mail.outbox) == 1
49+
50+
assert pending_email_1.status == SentEmail.Status.sent
51+
assert pending_email_1.message_id.startswith("local-")
52+
assert pending_email_1.sent_at.isoformat() == "2021-01-01T12:00:00+00:00"
4553

46-
def test_send_pending_emails_task_doesnt_double_send():
54+
55+
def test_send_pending_email_task_doesnt_double_send():
4756
pending_email_1 = SentEmailFactory(status=SentEmail.Status.pending)
4857
original_qs = SentEmail.objects.select_for_update(skip_locked=True).filter(
4958
id=pending_email_1.id
@@ -57,52 +66,17 @@ def side_effect(*args, **kwargs):
5766
"notifications.tasks.SentEmail.objects.select_for_update",
5867
side_effect=side_effect,
5968
):
60-
send_pending_emails()
69+
send_pending_email(pending_email_1.id)
6170

6271
pending_email_1.refresh_from_db()
6372

6473
assert len(mail.outbox) == 0
6574

6675

67-
def test_send_pending_emails_task():
68-
pending_email_1 = SentEmailFactory(status=SentEmail.Status.pending)
69-
pending_email_2 = SentEmailFactory(status=SentEmail.Status.pending)
70-
sent_email_1 = SentEmailFactory(
71-
status=SentEmail.Status.sent, message_id="abc-abc", sent_at="2020-01-01 12:00Z"
72-
)
73-
74-
with time_machine.travel("2021-01-01 12:00Z", tick=False):
75-
send_pending_emails()
76-
77-
pending_email_1.refresh_from_db()
78-
pending_email_2.refresh_from_db()
79-
sent_email_1.refresh_from_db()
80-
81-
assert len(mail.outbox) == 2
82-
83-
assert mail.outbox[0].to == [pending_email_1.recipient_email]
84-
assert mail.outbox[1].to == [pending_email_2.recipient_email]
85-
86-
assert pending_email_1.status == SentEmail.Status.sent
87-
assert pending_email_1.message_id.startswith("local-")
88-
assert pending_email_1.sent_at.isoformat() == "2021-01-01T12:00:00+00:00"
89-
90-
assert pending_email_2.status == SentEmail.Status.sent
91-
assert pending_email_2.message_id.startswith("local-")
92-
assert pending_email_2.sent_at.isoformat() == "2021-01-01T12:00:00+00:00"
93-
94-
assert sent_email_1.status == SentEmail.Status.sent
95-
assert sent_email_1.message_id == "abc-abc"
96-
assert sent_email_1.sent_at.isoformat() == "2020-01-01T12:00:00+00:00"
97-
98-
99-
def test_send_pending_emails_handles_failures(mocker):
76+
def test_send_pending_email_handles_failures(mocker):
10077
pending_email_1 = SentEmailFactory(
10178
status=SentEmail.Status.pending, created="2020-01-01 12:00Z"
10279
)
103-
pending_email_2 = SentEmailFactory(
104-
status=SentEmail.Status.pending, created="2020-01-02 12:00Z"
105-
)
10680

10781
original_method = SentEmail.mark_as_sent
10882

@@ -111,27 +85,19 @@ def _side_effect(*args, **kwargs):
11185
_side_effect.counter = 1
11286
raise ValueError("test")
11387

114-
return original_method(pending_email_2, *args, **kwargs)
88+
return original_method(pending_email_1, *args, **kwargs)
11589

11690
_side_effect.counter = 0
11791

11892
mocker.patch("notifications.tasks.SentEmail.mark_as_sent", side_effect=_side_effect)
11993

120-
with time_machine.travel("2021-01-01 12:00Z", tick=False):
121-
send_pending_emails()
94+
with time_machine.travel("2021-01-01 12:00Z", tick=False), mock.patch(
95+
"celery.app.task.Task.request"
96+
) as mock_task_request:
97+
mock_task_request.retries = 10
98+
send_pending_email(pending_email_1.id)
12299

123100
pending_email_1.refresh_from_db()
124-
pending_email_2.refresh_from_db()
125-
126-
assert len(mail.outbox) == 2
127-
128-
assert mail.outbox[0].to == [pending_email_1.recipient_email]
129-
assert mail.outbox[1].to == [pending_email_2.recipient_email]
130101

102+
assert len(mail.outbox) == 0
131103
assert pending_email_1.status == SentEmail.Status.failed
132-
assert pending_email_1.message_id == ""
133-
assert pending_email_1.sent_at is None
134-
135-
assert pending_email_2.status == SentEmail.Status.sent
136-
assert pending_email_2.message_id.startswith("local-")
137-
assert pending_email_2.sent_at.isoformat() == "2021-01-01T12:00:00+00:00"

backend/pycon/celery.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def setup_periodic_tasks(sender, **kwargs):
2424
from schedule.tasks import process_schedule_items_videos_to_upload
2525
from files_upload.tasks import delete_unused_files
2626
from pycon.tasks import check_for_idle_heavy_processing_workers
27-
from notifications.tasks import send_pending_emails
2827

2928
add = sender.add_periodic_task
3029

@@ -48,8 +47,3 @@ def setup_periodic_tasks(sender, **kwargs):
4847
check_for_idle_heavy_processing_workers,
4948
name="Check for idle heavy processing workers",
5049
)
51-
add(
52-
timedelta(minutes=1),
53-
send_pending_emails,
54-
name="Send pending emails",
55-
)

0 commit comments

Comments
 (0)