Merge lp:~gary/launchpad/bug741684-2 into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 12715
Proposed branch: lp:~gary/launchpad/bug741684-2
Merge into: lp:launchpad
Prerequisite: lp:~gary/launchpad/bug741684
Diff against target: 429 lines (+151/-74)
8 files modified
database/schema/security.cfg (+2/-0)
lib/canonical/launchpad/helpers.py (+5/-28)
lib/canonical/launchpad/tests/test_helpers.py (+0/-34)
lib/lp/bugs/scripts/bugnotification.py (+4/-5)
lib/lp/registry/model/person.py (+64/-0)
lib/lp/registry/tests/test_person.py (+69/-1)
lib/lp/services/mail/notificationrecipientset.py (+5/-4)
lib/lp/soyuz/scripts/ppareport.py (+2/-2)
To merge this branch: bzr merge lp:~gary/launchpad/bug741684-2
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+55657@code.launchpad.net

Commit message

[r=jcsackett][bug=741684,742230] Optimizations for the DB usage and performance of the bug notifications script.

Description of the change

This branch is the second of two that I have in mind to address the linked bugs. The other branch is the one on which this one depends, lp:~gary/launchpad/bug741684. The only additional change I know of at the moment may be to increase the Storm cache size, as discussed in the MP for the other branch.

This branch replaces canonical.launchpad.helpers.emailPeople with lib.lp.registry.model.person.get_recipients. The change in code is to make fewer passes through the transitive search, and to take advantage of the IPersonSet.getPrecachedPersonsFromIDs method to early-load email information that we need. The change in module is because the new code uses database tools that are not allowed to be imported from the scripts location. The change in name was because I already had to change all the call sites, and I thought it was an improved name, since I thought it more clearly described the function's purpose.

I briefly toyed with the idea of making this a method on the Person class. I really have wanted to see a move to tightly-focused data-based models, though, so I preferred to make this a function. That so, if you, the reviewer, request that it be a method, I will make it so with nary a complaint.

When migrating the tests of the old function to the new location, I expanded them. The test_get_recipients_complex_indirect in particular I used in experiments to show that even in a relatively simple case like that I was reducing database calls by half. I'm hopeful that in real-world usage, the change will be significantly more dramatic.

I incorporated the new version of the function everywhere the old one was used; I am running tests now to see if this causes any unpleasant hiccups. As you might expect, the usage I'm most interested in right now is the one in lib/lp/bugs/scripts/bugnotification.py.

Thank you for your time.

Gary

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good to land.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2011-03-29 03:35:23 +0000
+++ database/schema/security.cfg 2011-03-31 13:56:01 +0000
@@ -721,6 +721,7 @@
721[distributionmirror]721[distributionmirror]
722type=user722type=user
723groups=script723groups=script
724public.account = SELECT
724public.archive = SELECT725public.archive = SELECT
725public.archivearch = SELECT726public.archivearch = SELECT
726public.binarypackagefile = SELECT727public.binarypackagefile = SELECT
@@ -1650,6 +1651,7 @@
1650[translationstobranch]1651[translationstobranch]
1651type=user1652type=user
1652groups=script1653groups=script
1654public.account = SELECT
1653public.branch = SELECT, UPDATE1655public.branch = SELECT, UPDATE
1654public.branchjob = SELECT, UPDATE, INSERT1656public.branchjob = SELECT, UPDATE, INSERT
1655public.emailaddress = SELECT1657public.emailaddress = SELECT
16561658
=== modified file 'lib/canonical/launchpad/helpers.py'
--- lib/canonical/launchpad/helpers.py 2011-02-17 16:15:50 +0000
+++ lib/canonical/launchpad/helpers.py 2011-03-31 13:56:01 +0000
@@ -200,43 +200,20 @@
200 return output200 return output
201201
202202
203def emailPeople(person):
204 """Return a set of people to who receive email for this Person.
205
206 If <person> has a preferred email, the set will contain only that
207 person. If <person> doesn't have a preferred email but is a team,
208 the set will contain the preferred email address of each member of
209 <person>, including indirect members.
210
211 Finally, if <person> doesn't have a preferred email and is not a team,
212 the set will be empty.
213 """
214 pending_people = [person]
215 people = set()
216 seen = set()
217 while len(pending_people) > 0:
218 person = pending_people.pop()
219 if person in seen:
220 continue
221 seen.add(person)
222 if person.preferredemail is not None:
223 people.add(person)
224 elif person.isTeam():
225 pending_people.extend(person.activemembers)
226 return people
227
228
229def get_contact_email_addresses(person):203def get_contact_email_addresses(person):
230 """Return a set of email addresses to contact this Person.204 """Return a set of email addresses to contact this Person.
231205
232 In general, it is better to use emailPeople instead.206 In general, it is better to use lp.registry.model.person.get_recipients
207 instead.
233 """208 """
234 # Need to remove the security proxy of the email address because the209 # Need to remove the security proxy of the email address because the
235 # logged in user may not have permission to see it.210 # logged in user may not have permission to see it.
236 from zope.security.proxy import removeSecurityProxy211 from zope.security.proxy import removeSecurityProxy
212 # Circular imports force this import.
213 from lp.registry.model.person import get_recipients
237 return set(214 return set(
238 str(removeSecurityProxy(mail_person.preferredemail).email)215 str(removeSecurityProxy(mail_person.preferredemail).email)
239 for mail_person in emailPeople(person))216 for mail_person in get_recipients(person))
240217
241218
242replacements = {0: {'.': ' |dot| ',219replacements = {0: {'.': ' |dot| ',
243220
=== modified file 'lib/canonical/launchpad/tests/test_helpers.py'
--- lib/canonical/launchpad/tests/test_helpers.py 2010-10-21 04:19:36 +0000
+++ lib/canonical/launchpad/tests/test_helpers.py 2011-03-31 13:56:01 +0000
@@ -8,12 +8,9 @@
8from zope.publisher.interfaces.browser import IBrowserRequest8from zope.publisher.interfaces.browser import IBrowserRequest
99
10from canonical.launchpad import helpers10from canonical.launchpad import helpers
11from canonical.launchpad.ftests import login
12from canonical.launchpad.webapp.interfaces import ILaunchBag11from canonical.launchpad.webapp.interfaces import ILaunchBag
13from canonical.testing.layers import LaunchpadFunctionalLayer
14from lp.registry.interfaces.person import IPerson12from lp.registry.interfaces.person import IPerson
15from lp.services.worlddata.interfaces.language import ILanguageSet13from lp.services.worlddata.interfaces.language import ILanguageSet
16from lp.testing.factory import LaunchpadObjectFactory
17from lp.translations.utilities.translation_export import LaunchpadWriteTarFile14from lp.translations.utilities.translation_export import LaunchpadWriteTarFile
1815
1916
@@ -298,41 +295,10 @@
298 self.assertEqual(text, helpers.truncate_text(text, len(text)))295 self.assertEqual(text, helpers.truncate_text(text, len(text)))
299296
300297
301class TestEmailPeople(unittest.TestCase):
302 """Tests for emailPeople"""
303
304 layer = LaunchpadFunctionalLayer
305
306 def setUp(self):
307 unittest.TestCase.setUp(self)
308 login('foo.bar@canonical.com')
309 self.factory = LaunchpadObjectFactory()
310
311 def test_emailPeopleIndirect(self):
312 """Ensure emailPeople uses indirect memberships."""
313 owner = self.factory.makePerson(
314 displayname='Foo Bar', email='foo@bar.com', password='password')
315 team = self.factory.makeTeam(owner)
316 super_team = self.factory.makeTeam(team)
317 recipients = helpers.emailPeople(super_team)
318 self.assertEqual(set([owner]), recipients)
319
320 def test_emailPeopleTeam(self):
321 """Ensure emailPeople uses teams with preferredemail."""
322 owner = self.factory.makePerson(
323 displayname='Foo Bar', email='foo@bar.com', password='password')
324 team = self.factory.makeTeam(owner, email='team@bar.com')
325 super_team = self.factory.makeTeam(team)
326 recipients = helpers.emailPeople(super_team)
327 self.assertEqual(set([team]), recipients)
328
329
330def test_suite():298def test_suite():
331 suite = unittest.TestSuite()299 suite = unittest.TestSuite()
332 suite.addTest(DocTestSuite())300 suite.addTest(DocTestSuite())
333 suite.addTest(DocTestSuite(helpers))301 suite.addTest(DocTestSuite(helpers))
334 suite.addTest(302 suite.addTest(
335 unittest.TestLoader().loadTestsFromTestCase(TruncateTextTest))303 unittest.TestLoader().loadTestsFromTestCase(TruncateTextTest))
336 suite.addTest(
337 unittest.TestLoader().loadTestsFromTestCase(TestEmailPeople))
338 return suite304 return suite
339305
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2011-03-31 13:55:39 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2011-03-31 13:56:01 +0000
@@ -18,10 +18,7 @@
18import transaction18import transaction
19from zope.component import getUtility19from zope.component import getUtility
2020
21from canonical.launchpad.helpers import (21from canonical.launchpad.helpers import get_email_template
22 emailPeople,
23 get_email_template,
24 )
25from canonical.launchpad.scripts.logger import log22from canonical.launchpad.scripts.logger import log
26from canonical.launchpad.webapp import canonical_url23from canonical.launchpad.webapp import canonical_url
27from lp.bugs.mail.bugnotificationbuilder import (24from lp.bugs.mail.bugnotificationbuilder import (
@@ -30,6 +27,7 @@
30 )27 )
31from lp.bugs.mail.newbug import generate_bug_add_email28from lp.bugs.mail.newbug import generate_bug_add_email
32from lp.bugs.interfaces.bugnotification import IBugNotificationSet29from lp.bugs.interfaces.bugnotification import IBugNotificationSet
30from lp.registry.model.person import get_recipients
33from lp.services.mail.mailwrapper import MailWrapper31from lp.services.mail.mailwrapper import MailWrapper
3432
3533
@@ -108,7 +106,7 @@
108 # We will report this notification.106 # We will report this notification.
109 filtered_notifications.append(notification)107 filtered_notifications.append(notification)
110 for recipient in notification.recipients:108 for recipient in notification.recipients:
111 email_people = emailPeople(recipient.person)109 email_people = get_recipients(recipient.person)
112 for email_person in email_people:110 for email_person in email_people:
113 recipients_for_person = recipients.get(email_person)111 recipients_for_person = recipients.get(email_person)
114 if recipients_for_person is None:112 if recipients_for_person is None:
@@ -169,6 +167,7 @@
169 cached_filters = {}167 cached_filters = {}
170168
171 bug_notification_set = getUtility(IBugNotificationSet)169 bug_notification_set = getUtility(IBugNotificationSet)
170
172 def get_filter_descriptions(recipients):171 def get_filter_descriptions(recipients):
173 "Given a list of recipients, return the filter descriptions for them."172 "Given a list of recipients, return the filter descriptions for them."
174 descriptions = set()173 descriptions = set()
175174
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-03-29 03:13:40 +0000
+++ lib/lp/registry/model/person.py 2011-03-31 13:56:01 +0000
@@ -7,6 +7,7 @@
77
8__metaclass__ = type8__metaclass__ = type
9__all__ = [9__all__ = [
10 'get_recipients',
10 'generate_nick',11 'generate_nick',
11 'IrcID',12 'IrcID',
12 'IrcIDSet',13 'IrcIDSet',
@@ -57,6 +58,7 @@
57 And,58 And,
58 Desc,59 Desc,
59 Exists,60 Exists,
61 In,
60 Join,62 Join,
61 LeftJoin,63 LeftJoin,
62 Min,64 Min,
@@ -4561,3 +4563,65 @@
4561 if person is None:4563 if person is None:
4562 raise ComponentLookupError()4564 raise ComponentLookupError()
4563 return person4565 return person
4566
4567
4568@ProxyFactory
4569def get_recipients(person):
4570 """Return a set of people who receive email for this Person (person/team).
4571
4572 If <person> has a preferred email, the set will contain only that
4573 person. If <person> doesn't have a preferred email but is a team,
4574 the set will contain the preferred email address of each member of
4575 <person>, including indirect members.
4576
4577 Finally, if <person> doesn't have a preferred email and is not a team,
4578 the set will be empty.
4579 """
4580 if person.preferredemail:
4581 return [person]
4582 elif person.is_team:
4583 # Get transitive members of team with a preferred email.
4584 return _get_recipients_for_team(person)
4585 else:
4586 return []
4587
4588
4589def _get_recipients_for_team(team):
4590 """Given a team without a preferred email, return recipients.
4591
4592 Helper for get_recipients, divided out simply to make get_recipients
4593 easier to understand in broad brush."""
4594 store = IStore(Person)
4595 source = store.using(TeamMembership,
4596 Join(Person,
4597 TeamMembership.personID==Person.id),
4598 LeftJoin(EmailAddress,
4599 EmailAddress.person == Person.id))
4600 pending_team_ids = [team.id]
4601 recipient_ids = set()
4602 seen = set()
4603 while pending_team_ids:
4604 intermediate_transitive_results = source.find(
4605 (TeamMembership.personID, EmailAddress.personID),
4606 In(TeamMembership.status,
4607 [TeamMembershipStatus.ADMIN.value,
4608 TeamMembershipStatus.APPROVED.value]),
4609 In(TeamMembership.teamID, pending_team_ids),
4610 Or(EmailAddress.status == EmailAddressStatus.PREFERRED,
4611 And(Not(Person.teamownerID == None),
4612 EmailAddress.status == None))).config(distinct=True)
4613 next_ids = []
4614 for (person_id,
4615 preferred_email_marker) in intermediate_transitive_results:
4616 if preferred_email_marker is None:
4617 # This is a team without a preferred email address.
4618 if person_id not in seen:
4619 next_ids.append(person_id)
4620 seen.add(person_id)
4621 else:
4622 recipient_ids.add(person_id)
4623 pending_team_ids = next_ids
4624 return getUtility(IPersonSet).getPrecachedPersonsFromIDs(
4625 recipient_ids,
4626 need_validity=True,
4627 need_preferred_email=True)
45644628
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-03-26 16:28:39 +0000
+++ lib/lp/registry/tests/test_person.py 2011-03-31 13:56:01 +0000
@@ -35,6 +35,7 @@
35from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller35from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
36from canonical.testing.layers import (36from canonical.testing.layers import (
37 DatabaseFunctionalLayer,37 DatabaseFunctionalLayer,
38 LaunchpadFunctionalLayer,
38 reconnect_stores,39 reconnect_stores,
39 )40 )
40from lp.answers.model.answercontact import AnswerContact41from lp.answers.model.answercontact import AnswerContact
@@ -61,7 +62,10 @@
61 KarmaCategory,62 KarmaCategory,
62 KarmaTotalCache,63 KarmaTotalCache,
63 )64 )
64from lp.registry.model.person import Person65from lp.registry.model.person import (
66 get_recipients,
67 Person,
68 )
65from lp.services.openid.model.openididentifier import OpenIdIdentifier69from lp.services.openid.model.openididentifier import OpenIdIdentifier
66from lp.soyuz.enums import (70from lp.soyuz.enums import (
67 ArchivePurpose,71 ArchivePurpose,
@@ -1293,3 +1297,67 @@
1293 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 31297 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 3
1294 # queries to the test.1298 # queries to the test.
1295 self.assertThat(collector, HasQueryCount(LessThan(14)))1299 self.assertThat(collector, HasQueryCount(LessThan(14)))
1300
1301
1302class TestGetRecipients(TestCaseWithFactory):
1303 """Tests for get_recipients"""
1304
1305 layer = LaunchpadFunctionalLayer
1306
1307 def setUp(self):
1308 super(TestGetRecipients, self).setUp()
1309 login('foo.bar@canonical.com')
1310
1311 def test_get_recipients_indirect(self):
1312 """Ensure get_recipients uses indirect memberships."""
1313 owner = self.factory.makePerson(
1314 displayname='Foo Bar', email='foo@bar.com', password='password')
1315 team = self.factory.makeTeam(owner)
1316 super_team = self.factory.makeTeam(team)
1317 recipients = get_recipients(super_team)
1318 self.assertEqual(set([owner]), set(recipients))
1319
1320 def test_get_recipients_team(self):
1321 """Ensure get_recipients uses teams with preferredemail."""
1322 owner = self.factory.makePerson(
1323 displayname='Foo Bar', email='foo@bar.com', password='password')
1324 team = self.factory.makeTeam(owner, email='team@bar.com')
1325 super_team = self.factory.makeTeam(team)
1326 recipients = get_recipients(super_team)
1327 self.assertEqual(set([team]), set(recipients))
1328
1329 def makePersonWithNoPreferredEmail(self, **kwargs):
1330 kwargs['email_address_status'] = EmailAddressStatus.NEW
1331 return self.factory.makePerson(**kwargs)
1332
1333 def get_test_recipients_person(self):
1334 person = self.factory.makePerson()
1335 recipients = get_recipients(person)
1336 self.assertEqual(set([person]), set(recipients))
1337
1338 def test_get_recipients_empty(self):
1339 """get_recipients returns empty set for person with no preferredemail.
1340 """
1341 recipients = get_recipients(self.makePersonWithNoPreferredEmail())
1342 self.assertEqual(set(), set(recipients))
1343
1344 def test_get_recipients_complex_indirect(self):
1345 """Ensure get_recipients uses indirect memberships."""
1346 owner = self.factory.makePerson(
1347 displayname='Foo Bar', email='foo@bar.com', password='password')
1348 team = self.factory.makeTeam(owner)
1349 super_team_member_person = self.factory.makePerson(
1350 displayname='Bing Bar', email='bing@bar.com')
1351 super_team_member_team = self.factory.makeTeam(
1352 email='baz@bar.com')
1353 super_team = self.factory.makeTeam(
1354 team, members=[super_team_member_person,
1355 super_team_member_team,
1356 self.makePersonWithNoPreferredEmail()])
1357 super_team_member_team.acceptInvitationToBeMemberOf(
1358 super_team, u'Go Team!')
1359 recipients = list(get_recipients(super_team))
1360 self.assertEqual(set([owner,
1361 super_team_member_person,
1362 super_team_member_team]),
1363 set(recipients))
12961364
=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
--- lib/lp/services/mail/notificationrecipientset.py 2010-12-02 16:13:51 +0000
+++ lib/lp/services/mail/notificationrecipientset.py 2011-03-31 13:56:01 +0000
@@ -14,7 +14,6 @@
14from zope.interface import implements14from zope.interface import implements
15from zope.security.proxy import isinstance as zope_isinstance15from zope.security.proxy import isinstance as zope_isinstance
1616
17from canonical.launchpad.helpers import emailPeople
18from canonical.launchpad.interfaces.launchpad import (17from canonical.launchpad.interfaces.launchpad import (
19 INotificationRecipientSet,18 INotificationRecipientSet,
20 UnknownRecipientError,19 UnknownRecipientError,
@@ -45,7 +44,7 @@
45 def getRecipients(self):44 def getRecipients(self):
46 """See `INotificationRecipientSet`."""45 """See `INotificationRecipientSet`."""
47 return sorted(46 return sorted(
48 self._personToRationale.keys(), key=attrgetter('displayname'))47 self._personToRationale.keys(), key=attrgetter('displayname'))
4948
50 def getRecipientPersons(self):49 def getRecipientPersons(self):
51 """See `INotificationRecipientSet`."""50 """See `INotificationRecipientSet`."""
@@ -88,6 +87,7 @@
88 def add(self, persons, reason, header):87 def add(self, persons, reason, header):
89 """See `INotificationRecipientSet`."""88 """See `INotificationRecipientSet`."""
90 from zope.security.proxy import removeSecurityProxy89 from zope.security.proxy import removeSecurityProxy
90 from lp.registry.model.person import get_recipients
91 if IPerson.providedBy(persons):91 if IPerson.providedBy(persons):
92 persons = [persons]92 persons = [persons]
9393
@@ -98,7 +98,7 @@
98 if person in self._personToRationale:98 if person in self._personToRationale:
99 continue99 continue
100 self._personToRationale[person] = reason, header100 self._personToRationale[person] = reason, header
101 for receiving_person in emailPeople(person):101 for receiving_person in get_recipients(person):
102 # Bypass zope's security because IEmailAddress.email is not102 # Bypass zope's security because IEmailAddress.email is not
103 # public.103 # public.
104 preferred_email = removeSecurityProxy(104 preferred_email = removeSecurityProxy(
@@ -116,6 +116,7 @@
116 def remove(self, persons):116 def remove(self, persons):
117 """See `INotificationRecipientSet`."""117 """See `INotificationRecipientSet`."""
118 from zope.security.proxy import removeSecurityProxy118 from zope.security.proxy import removeSecurityProxy
119 from lp.registry.model.person import get_recipients
119 if IPerson.providedBy(persons):120 if IPerson.providedBy(persons):
120 persons = [persons]121 persons = [persons]
121 for person in persons:122 for person in persons:
@@ -123,7 +124,7 @@
123 'You can only remove() an IPerson: %r' % person)124 'You can only remove() an IPerson: %r' % person)
124 if person in self._personToRationale:125 if person in self._personToRationale:
125 del self._personToRationale[person]126 del self._personToRationale[person]
126 for removed_person in emailPeople(person):127 for removed_person in get_recipients(person):
127 # Bypass zope's security because IEmailAddress.email is128 # Bypass zope's security because IEmailAddress.email is
128 # not public.129 # not public.
129 preferred_email = removeSecurityProxy(130 preferred_email = removeSecurityProxy(
130131
=== modified file 'lib/lp/soyuz/scripts/ppareport.py'
--- lib/lp/soyuz/scripts/ppareport.py 2010-08-31 11:11:09 +0000
+++ lib/lp/soyuz/scripts/ppareport.py 2011-03-31 13:56:01 +0000
@@ -20,9 +20,9 @@
20from zope.component import getUtility20from zope.component import getUtility
2121
22from canonical.config import config22from canonical.config import config
23from canonical.launchpad.helpers import emailPeople
24from canonical.launchpad.webapp import canonical_url23from canonical.launchpad.webapp import canonical_url
25from lp.registry.interfaces.distribution import IDistributionSet24from lp.registry.interfaces.distribution import IDistributionSet
25from lp.registry.model.person import get_recipients
26from lp.services.propertycache import cachedproperty26from lp.services.propertycache import cachedproperty
27from lp.services.scripts.base import (27from lp.services.scripts.base import (
28 LaunchpadScript,28 LaunchpadScript,
@@ -181,7 +181,7 @@
181 self.output.write('= PPA user emails =\n')181 self.output.write('= PPA user emails =\n')
182 people_to_email = set()182 people_to_email = set()
183 for ppa in self.ppas:183 for ppa in self.ppas:
184 people_to_email.update(emailPeople(ppa.owner))184 people_to_email.update(get_recipients(ppa.owner))
185 sorted_people_to_email = sorted(185 sorted_people_to_email = sorted(
186 people_to_email, key=operator.attrgetter('name'))186 people_to_email, key=operator.attrgetter('name'))
187 for user in sorted_people_to_email:187 for user in sorted_people_to_email: