Merge lp:~cjwatson/launchpad/send-bug-notifications-oops into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17682
Proposed branch: lp:~cjwatson/launchpad/send-bug-notifications-oops
Merge into: lp:launchpad
Diff against target: 290 lines (+143/-47)
3 files modified
cronscripts/send-bug-notifications.py (+1/-44)
lib/lp/bugs/scripts/bugnotification.py (+68/-2)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+74/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/send-bug-notifications-oops
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+264814@code.launchpad.net

Commit message

Turn SMTPExceptions when sending bug notifications into OOPSes rather than letting the entire script crash.

Description of the change

Turn SMTPExceptions when sending bug notifications into OOPSes rather than letting the entire script crash.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Needs Fixing (code)
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
William Grant (wgrant) wrote :

Thanks. I'd also include the BugNotification ID for easy debugging.

review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) wrote :

As noted on IRC, this is rather too cumbersome at the moment because construct_email_notifications doesn't keep a clear record of which notification generated which message.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cronscripts/send-bug-notifications.py'
--- cronscripts/send-bug-notifications.py 2013-01-07 02:40:55 +0000
+++ cronscripts/send-bug-notifications.py 2015-07-21 15:47:29 +0000
@@ -13,51 +13,8 @@
1313
14import _pythonpath14import _pythonpath
1515
16from zope.component import getUtility16from lp.bugs.scripts.bugnotification import SendBugNotifications
17
18from lp.bugs.enums import BugNotificationStatus
19from lp.bugs.interfaces.bugnotification import IBugNotificationSet
20from lp.bugs.scripts.bugnotification import (
21 get_email_notifications,
22 process_deferred_notifications,
23 )
24from lp.services.config import config17from lp.services.config import config
25from lp.services.database.constants import UTC_NOW
26from lp.services.mail.sendmail import sendmail
27from lp.services.scripts.base import LaunchpadCronScript
28
29
30class SendBugNotifications(LaunchpadCronScript):
31 def main(self):
32 notifications_sent = False
33 bug_notification_set = getUtility(IBugNotificationSet)
34 deferred_notifications = \
35 bug_notification_set.getDeferredNotifications()
36 process_deferred_notifications(deferred_notifications)
37 pending_notifications = get_email_notifications(
38 bug_notification_set.getNotificationsToSend())
39 for (bug_notifications,
40 omitted_notifications,
41 messages) in pending_notifications:
42 for message in messages:
43 self.logger.info("Notifying %s about bug %d." % (
44 message['To'], bug_notifications[0].bug.id))
45 sendmail(message)
46 self.logger.debug(message.as_string())
47 for notification in bug_notifications:
48 notification.date_emailed = UTC_NOW
49 notification.status = BugNotificationStatus.SENT
50 for notification in omitted_notifications:
51 notification.date_emailed = UTC_NOW
52 notification.status = BugNotificationStatus.OMITTED
53 notifications_sent = True
54 # Commit after each batch of email sent, so that we won't
55 # re-mail the notifications in case of something going wrong
56 # in the middle.
57 self.txn.commit()
58
59 if not notifications_sent:
60 self.logger.debug("No notifications are pending to be sent.")
6118
6219
63if __name__ == '__main__':20if __name__ == '__main__':
6421
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2015-07-06 17:27:09 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2015-07-21 15:47:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Functions related to sending bug notifications."""4"""Functions related to sending bug notifications."""
@@ -13,12 +13,18 @@
1313
14from itertools import groupby14from itertools import groupby
15from operator import itemgetter15from operator import itemgetter
16from smtplib import SMTPException
17import sys
1618
17from storm.store import Store19from storm.store import Store
18import transaction20import transaction
19from zope.component import getUtility21from zope.component import getUtility
22from zope.error.interfaces import IErrorReportingUtility
2023
21from lp.bugs.enums import BugNotificationLevel24from lp.bugs.enums import (
25 BugNotificationLevel,
26 BugNotificationStatus,
27 )
22from lp.bugs.interfaces.bugnotification import IBugNotificationSet28from lp.bugs.interfaces.bugnotification import IBugNotificationSet
23from lp.bugs.mail.bugnotificationbuilder import (29from lp.bugs.mail.bugnotificationbuilder import (
24 BugNotificationBuilder,30 BugNotificationBuilder,
@@ -26,10 +32,14 @@
26 )32 )
27from lp.bugs.mail.newbug import generate_bug_add_email33from lp.bugs.mail.newbug import generate_bug_add_email
28from lp.registry.model.person import get_recipients34from lp.registry.model.person import get_recipients
35from lp.services.database.constants import UTC_NOW
29from lp.services.mail.helpers import get_email_template36from lp.services.mail.helpers import get_email_template
30from lp.services.mail.mailwrapper import MailWrapper37from lp.services.mail.mailwrapper import MailWrapper
38from lp.services.mail.sendmail import sendmail
39from lp.services.scripts.base import LaunchpadCronScript
31from lp.services.scripts.logger import log40from lp.services.scripts.logger import log
32from lp.services.webapp import canonical_url41from lp.services.webapp import canonical_url
42from lp.services.webapp.errorlog import ScriptRequest
3343
3444
35def get_activity_key(notification):45def get_activity_key(notification):
@@ -327,3 +337,59 @@
327 message=message,337 message=message,
328 recipients=recipients,338 recipients=recipients,
329 activity=activity)339 activity=activity)
340
341
342class SendBugNotifications(LaunchpadCronScript):
343 def main(self):
344 notifications_sent = False
345 bug_notification_set = getUtility(IBugNotificationSet)
346 deferred_notifications = \
347 bug_notification_set.getDeferredNotifications()
348 process_deferred_notifications(deferred_notifications)
349 pending_notifications = get_email_notifications(
350 bug_notification_set.getNotificationsToSend())
351 for (bug_notifications,
352 omitted_notifications,
353 messages) in pending_notifications:
354 try:
355 for message in messages:
356 try:
357 self.logger.info("Notifying %s about bug %d." % (
358 message['To'], bug_notifications[0].bug.id))
359 sendmail(message)
360 self.logger.debug(message.as_string())
361 except SMTPException:
362 request = ScriptRequest([
363 ("script_name", self.name),
364 ("path", sys.argv[0]),
365 ])
366 error_utility = getUtility(IErrorReportingUtility)
367 oops_vars = {
368 "message_id": message.get("Message-Id"),
369 "notification_type": "bug",
370 "recipient": message["To"],
371 "subject": message["Subject"],
372 }
373 with error_utility.oopsMessage(oops_vars):
374 error_utility.raising(sys.exc_info(), request)
375 self.logger.info(request.oopsid)
376 self.txn.abort()
377 # Re-raise to get out of this loop and go to the
378 # next iteration of the outer loop.
379 raise
380 except SMTPException:
381 continue
382 for notification in bug_notifications:
383 notification.date_emailed = UTC_NOW
384 notification.status = BugNotificationStatus.SENT
385 for notification in omitted_notifications:
386 notification.date_emailed = UTC_NOW
387 notification.status = BugNotificationStatus.OMITTED
388 notifications_sent = True
389 # Commit after each batch of email sent, so that we won't
390 # re-mail the notifications in case of something going wrong
391 # in the middle.
392 self.txn.commit()
393
394 if not notifications_sent:
395 self.logger.debug("No notifications are pending to be sent.")
330396
=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2015-07-21 15:47:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3"""Tests for construction bug notification emails for sending."""3"""Tests for construction bug notification emails for sending."""
44
@@ -10,6 +10,7 @@
10 )10 )
11import logging11import logging
12import re12import re
13from smtplib import SMTPException
13import StringIO14import StringIO
14import unittest15import unittest
1516
@@ -22,6 +23,7 @@
22 getUtility,23 getUtility,
23 )24 )
24from zope.interface import implementer25from zope.interface import implementer
26from zope.sendmail.interfaces import IMailDelivery
2527
26from lp.app.enums import InformationType28from lp.app.enums import InformationType
27from lp.bugs.adapters.bugchange import (29from lp.bugs.adapters.bugchange import (
@@ -38,6 +40,7 @@
38 CveLinkedToBug,40 CveLinkedToBug,
39 CveUnlinkedFromBug,41 CveUnlinkedFromBug,
40 )42 )
43from lp.bugs.enums import BugNotificationStatus
41from lp.bugs.interfaces.bug import (44from lp.bugs.interfaces.bug import (
42 CreateBugParams,45 CreateBugParams,
43 IBug,46 IBug,
@@ -63,6 +66,7 @@
63 notification_batches,66 notification_batches,
64 notification_comment_batches,67 notification_comment_batches,
65 process_deferred_notifications,68 process_deferred_notifications,
69 SendBugNotifications,
66 )70 )
67from lp.registry.interfaces.person import IPersonSet71from lp.registry.interfaces.person import IPersonSet
68from lp.registry.interfaces.product import IProductSet72from lp.registry.interfaces.product import IProductSet
@@ -76,6 +80,8 @@
76 get_contact_email_addresses,80 get_contact_email_addresses,
77 get_email_template,81 get_email_template,
78 )82 )
83from lp.services.mail.sendmail import set_immediate_mail_delivery
84from lp.services.mail.stub import TestMailer
79from lp.services.messages.interfaces.message import IMessageSet85from lp.services.messages.interfaces.message import IMessageSet
80from lp.services.propertycache import cachedproperty86from lp.services.propertycache import cachedproperty
81from lp.testing import (87from lp.testing import (
@@ -87,6 +93,7 @@
87 lp_dbuser,93 lp_dbuser,
88 switch_dbuser,94 switch_dbuser,
89 )95 )
96from lp.testing.fixture import ZopeUtilityFixture
90from lp.testing.layers import LaunchpadZopelessLayer97from lp.testing.layers import LaunchpadZopelessLayer
91from lp.testing.matchers import Contains98from lp.testing.matchers import Contains
9299
@@ -1281,3 +1288,69 @@
1281 # And there are no longer any deferred.1288 # And there are no longer any deferred.
1282 deferred = self.notification_set.getDeferredNotifications()1289 deferred = self.notification_set.getDeferredNotifications()
1283 self.assertEqual(0, deferred.count())1290 self.assertEqual(0, deferred.count())
1291
1292
1293class BrokenMailer(TestMailer):
1294 """A mailer that raises an exception for certain recipient addresses."""
1295
1296 def __init__(self, broken):
1297 self.broken = broken
1298
1299 def send(self, from_addr, to_addrs, message):
1300 if any(to_addr in self.broken for to_addr in to_addrs):
1301 raise SMTPException("test requested delivery failure")
1302 return super(BrokenMailer, self).send(from_addr, to_addrs, message)
1303
1304
1305class TestSendBugNotifications(TestCaseWithFactory):
1306
1307 layer = LaunchpadZopelessLayer
1308
1309 def setUp(self):
1310 super(TestSendBugNotifications, self).setUp()
1311 self.notification_set = getUtility(IBugNotificationSet)
1312 # Ensure there are no outstanding notifications.
1313 for notification in self.notification_set.getNotificationsToSend():
1314 notification.destroySelf()
1315 self.ten_minutes_ago = datetime.now(pytz.UTC) - timedelta(minutes=10)
1316
1317 def test_oops_on_failed_delivery(self):
1318 # If one notification fails to send, it logs an OOPS and doesn't get
1319 # in the way of sending other notifications.
1320 set_immediate_mail_delivery(False)
1321 subscribers = []
1322 for i in range(3):
1323 bug = self.factory.makeBug()
1324 subscriber = self.factory.makePerson()
1325 subscribers.append(subscriber.preferredemail.email)
1326 bug.default_bugtask.target.addSubscription(subscriber, subscriber)
1327 message = getUtility(IMessageSet).fromText(
1328 "subject", "a comment.", bug.owner,
1329 datecreated=self.ten_minutes_ago)
1330 bug.addCommentNotification(message)
1331
1332 notifications = list(get_email_notifications(
1333 self.notification_set.getNotificationsToSend()))
1334 self.assertEqual(3, len(notifications))
1335
1336 mailer = BrokenMailer([subscribers[1]])
1337 with ZopeUtilityFixture(mailer, IMailDelivery, "Mail"):
1338 switch_dbuser(config.malone.bugnotification_dbuser)
1339 script = SendBugNotifications(
1340 "send-bug-notifications", config.malone.bugnotification_dbuser,
1341 ["-q"])
1342 script.txn = self.layer.txn
1343 script.main()
1344
1345 self.assertEqual(1, len(self.oopses))
1346 self.assertIn(
1347 "SMTPException: test requested delivery failure",
1348 self.oopses[0]["tb_text"])
1349 for i, (bug_notifications, _, messages) in enumerate(notifications):
1350 for bug_notification in bug_notifications:
1351 if i == 1:
1352 self.assertEqual(
1353 BugNotificationStatus.PENDING, bug_notification.status)
1354 else:
1355 self.assertEqual(
1356 BugNotificationStatus.SENT, bug_notification.status)