Skip to content

Commit

Permalink
+EMAIL_QUEUE_DISCARD_HOURS +EMAIL_QUEUE_RETRY_SECONDS
Browse files Browse the repository at this point in the history
  • Loading branch information
wooyek committed May 4, 2018
1 parent 24bbb17 commit 57d3e4c
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/django_email_queue/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@

#: Sleep time between queue checkup
EMAIL_QUEUE_SLEEP_TIME = 15

#: Discard messages after failing delivery for X hours
EMAIL_QUEUE_DISCARD_HOURS = None

#: Retry stalled messages (status==sending) after X seconds
EMAIL_QUEUE_RETRY_SECONDS = 300
24 changes: 24 additions & 0 deletions src/django_email_queue/migrations/0003_auto_20180504_0956.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.0.3 on 2018-05-04 09:56

from django.db import migrations, models
import django_email_queue.models


class Migration(migrations.Migration):

dependencies = [
('django_email_queue', '0002_auto_20180412_1413'),
]

operations = [
migrations.AddField(
model_name='queuedemailmessage',
name='ts',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='queuedemailmessage',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'Created'), (1, 'Posted'), (2, 'Sending'), (3, 'Discarded')], default=django_email_queue.models.QueuedEmailMessageStatus(0)),
),
]
26 changes: 24 additions & 2 deletions src/django_email_queue/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
from __future__ import absolute_import, unicode_literals

import logging
from datetime import timedelta
from enum import IntEnum

import six
from django.conf import settings
from django.core.mail.message import EmailMultiAlternatives
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

Expand All @@ -30,6 +32,7 @@ class QueuedEmailMessageStatus(ChoicesIntEnum):
created = 0
posted = 1
sending = 2
discarded = 3


HTML_MIME_TYPE = 'text/html'
Expand All @@ -41,6 +44,7 @@ class QueuedEmailMessage(models.Model):
status = models.PositiveSmallIntegerField(choices=QueuedEmailMessageStatus.choices(), default=QueuedEmailMessageStatus.created)
created = models.DateTimeField(auto_now_add=True)
posted = models.DateTimeField(blank=True, null=True)
ts = models.DateTimeField(blank=True, null=True, auto_now=True)
encoding = models.CharField(max_length=15, blank=True, null=True)
from_email = models.CharField(max_length=300)
to = models.TextField(blank=True, null=True)
Expand Down Expand Up @@ -90,7 +94,20 @@ def queue(cls, email_messages):
logging.debug("Queue message: %s", instance)
yield instance

def send(self, connection=None, fail_silently=False):
def send(self, connection=None, fail_silently=True):
try:
return self._send(connection=connection, fail_silently=False)
except Exception as ex:
if fail_silently is False:
raise ex
# Log error but do not block other messages
logging.error("Email send failed", exc_info=ex)
age_hours = (timezone.now() - self.created).seconds / 3600
if settings.EMAIL_QUEUE_DISCARD_HOURS and age_hours > settings.EMAIL_QUEUE_DISCARD_HOURS:
self.status = QueuedEmailMessageStatus.discarded
self.save()

def _send(self, connection=None, fail_silently=False):
if connection is None:
from django.core.mail import get_connection
connection = get_connection(
Expand Down Expand Up @@ -125,7 +142,12 @@ def send(self, connection=None, fail_silently=False):

@classmethod
def send_queued(cls, limit=None):
qry = cls.objects.filter(status=QueuedEmailMessageStatus.created).order_by("created")
seconds = settings.EMAIL_QUEUE_RETRY_SECONDS
retry = timezone.now() - timedelta(seconds=seconds)
qry = cls.objects.filter(
Q(status=QueuedEmailMessageStatus.created) |
Q(status=QueuedEmailMessageStatus.sending, ts__lte=retry)
).order_by("created")
if limit:
qry = qry[:limit]
cls.bulk_send(qry)
Expand Down
66 changes: 65 additions & 1 deletion tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import absolute_import, unicode_literals

import os
from datetime import timedelta

import pytest
import six
Expand All @@ -18,6 +19,7 @@
from django_email_queue import backends, models
from django_email_queue.admin import QueuedEmailMessageAdmin
from django_email_queue.models import QueuedEmailMessage, QueuedEmailMessageStatus
from tests import factories
from tests.factories import QueuedEmailMessageFactory


Expand Down Expand Up @@ -123,10 +125,72 @@ def test_str(self):
o = QueuedEmailMessage(subject="ążśźćń󳥯ŚŹĘĆŃÓŁ", to="b")
self.assertEqual(u"QueuedEmailMessage:ążśźćń󳥯ŚŹĘĆŃÓŁ:b", six.text_type(o))

@override_settings(EMAIL_QUEUE_DISCARD_HOURS=2)
@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_discard_message(self, send):
send.side_effect = Exception("Foo")
item = models.QueuedEmailMessage(created=timezone.now() - timedelta(hours=2, seconds=1))
item.send()
self.assertEqual(item.status, models.QueuedEmailMessageStatus.discarded)

@override_settings(EMAIL_QUEUE_DISCARD_HOURS=1)
@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_dont_discard_message(self, send):
send.side_effect = Exception("Foo")
item = models.QueuedEmailMessage(created=timezone.now() - timedelta(seconds=3599))
item.send()
self.assertEqual(models.QueuedEmailMessageStatus.created, item.status)

@override_settings(EMAIL_QUEUE_DISCARD_HOURS=None)
@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_dont_discard_message2(self, send):
send.side_effect = Exception("Foo")
item = models.QueuedEmailMessage(created=timezone.now() - timedelta(seconds=3599))
item.send()
self.assertEqual(models.QueuedEmailMessageStatus.created, item.status)

@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_fail_silently(self, send):
send.side_effect = Exception("Foo")
item = models.QueuedEmailMessage(created=timezone.now())
item.send()

@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_dont_fail_silently(self, send):
send.side_effect = Exception("Foo")
item = models.QueuedEmailMessage()
self.assertRaises(Exception, item.send, fail_silently=False)

@override_settings(EMAIL_QUEUE_RETRY_SECONDS=600)
@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_retry_busy(self, send):
seconds = settings.EMAIL_QUEUE_RETRY_SECONDS + 2
retry = timezone.now() - timedelta(seconds=seconds)
message = factories.QueuedEmailMessageFactory(
ts=retry,
status=models.QueuedEmailMessageStatus.sending,
)
models.QueuedEmailMessage.objects.all().update(ts=retry)
message.send_queued()
self.assertTrue(send.called)

@override_settings(EMAIL_QUEUE_RETRY_SECONDS=600)
@patch('django_email_queue.models.QueuedEmailMessage._send')
def test_retry_wait(self, send):
seconds = settings.EMAIL_QUEUE_RETRY_SECONDS - 2
retry = timezone.now() - timedelta(seconds=seconds)
message = factories.QueuedEmailMessageFactory(
ts=retry,
status=models.QueuedEmailMessageStatus.sending,
)
models.QueuedEmailMessage.objects.all().update(ts=retry)
message.send_queued()
self.assertFalse(send.called)


class QueuedEmailMessageStatusTests(TestCase):
def test_values(self):
self.assertEqual([0, 1, 2], QueuedEmailMessageStatus.values())
self.assertEqual([0, 1, 2, 3], QueuedEmailMessageStatus.values())


class CommandTests(TestCase):
Expand Down

0 comments on commit 57d3e4c

Please sign in to comment.