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
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-03-29 03:35:23 +0000
3+++ database/schema/security.cfg 2011-03-31 13:56:01 +0000
4@@ -721,6 +721,7 @@
5 [distributionmirror]
6 type=user
7 groups=script
8+public.account = SELECT
9 public.archive = SELECT
10 public.archivearch = SELECT
11 public.binarypackagefile = SELECT
12@@ -1650,6 +1651,7 @@
13 [translationstobranch]
14 type=user
15 groups=script
16+public.account = SELECT
17 public.branch = SELECT, UPDATE
18 public.branchjob = SELECT, UPDATE, INSERT
19 public.emailaddress = SELECT
20
21=== modified file 'lib/canonical/launchpad/helpers.py'
22--- lib/canonical/launchpad/helpers.py 2011-02-17 16:15:50 +0000
23+++ lib/canonical/launchpad/helpers.py 2011-03-31 13:56:01 +0000
24@@ -200,43 +200,20 @@
25 return output
26
27
28-def emailPeople(person):
29- """Return a set of people to who receive email for this Person.
30-
31- If <person> has a preferred email, the set will contain only that
32- person. If <person> doesn't have a preferred email but is a team,
33- the set will contain the preferred email address of each member of
34- <person>, including indirect members.
35-
36- Finally, if <person> doesn't have a preferred email and is not a team,
37- the set will be empty.
38- """
39- pending_people = [person]
40- people = set()
41- seen = set()
42- while len(pending_people) > 0:
43- person = pending_people.pop()
44- if person in seen:
45- continue
46- seen.add(person)
47- if person.preferredemail is not None:
48- people.add(person)
49- elif person.isTeam():
50- pending_people.extend(person.activemembers)
51- return people
52-
53-
54 def get_contact_email_addresses(person):
55 """Return a set of email addresses to contact this Person.
56
57- In general, it is better to use emailPeople instead.
58+ In general, it is better to use lp.registry.model.person.get_recipients
59+ instead.
60 """
61 # Need to remove the security proxy of the email address because the
62 # logged in user may not have permission to see it.
63 from zope.security.proxy import removeSecurityProxy
64+ # Circular imports force this import.
65+ from lp.registry.model.person import get_recipients
66 return set(
67 str(removeSecurityProxy(mail_person.preferredemail).email)
68- for mail_person in emailPeople(person))
69+ for mail_person in get_recipients(person))
70
71
72 replacements = {0: {'.': ' |dot| ',
73
74=== modified file 'lib/canonical/launchpad/tests/test_helpers.py'
75--- lib/canonical/launchpad/tests/test_helpers.py 2010-10-21 04:19:36 +0000
76+++ lib/canonical/launchpad/tests/test_helpers.py 2011-03-31 13:56:01 +0000
77@@ -8,12 +8,9 @@
78 from zope.publisher.interfaces.browser import IBrowserRequest
79
80 from canonical.launchpad import helpers
81-from canonical.launchpad.ftests import login
82 from canonical.launchpad.webapp.interfaces import ILaunchBag
83-from canonical.testing.layers import LaunchpadFunctionalLayer
84 from lp.registry.interfaces.person import IPerson
85 from lp.services.worlddata.interfaces.language import ILanguageSet
86-from lp.testing.factory import LaunchpadObjectFactory
87 from lp.translations.utilities.translation_export import LaunchpadWriteTarFile
88
89
90@@ -298,41 +295,10 @@
91 self.assertEqual(text, helpers.truncate_text(text, len(text)))
92
93
94-class TestEmailPeople(unittest.TestCase):
95- """Tests for emailPeople"""
96-
97- layer = LaunchpadFunctionalLayer
98-
99- def setUp(self):
100- unittest.TestCase.setUp(self)
101- login('foo.bar@canonical.com')
102- self.factory = LaunchpadObjectFactory()
103-
104- def test_emailPeopleIndirect(self):
105- """Ensure emailPeople uses indirect memberships."""
106- owner = self.factory.makePerson(
107- displayname='Foo Bar', email='foo@bar.com', password='password')
108- team = self.factory.makeTeam(owner)
109- super_team = self.factory.makeTeam(team)
110- recipients = helpers.emailPeople(super_team)
111- self.assertEqual(set([owner]), recipients)
112-
113- def test_emailPeopleTeam(self):
114- """Ensure emailPeople uses teams with preferredemail."""
115- owner = self.factory.makePerson(
116- displayname='Foo Bar', email='foo@bar.com', password='password')
117- team = self.factory.makeTeam(owner, email='team@bar.com')
118- super_team = self.factory.makeTeam(team)
119- recipients = helpers.emailPeople(super_team)
120- self.assertEqual(set([team]), recipients)
121-
122-
123 def test_suite():
124 suite = unittest.TestSuite()
125 suite.addTest(DocTestSuite())
126 suite.addTest(DocTestSuite(helpers))
127 suite.addTest(
128 unittest.TestLoader().loadTestsFromTestCase(TruncateTextTest))
129- suite.addTest(
130- unittest.TestLoader().loadTestsFromTestCase(TestEmailPeople))
131 return suite
132
133=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
134--- lib/lp/bugs/scripts/bugnotification.py 2011-03-31 13:55:39 +0000
135+++ lib/lp/bugs/scripts/bugnotification.py 2011-03-31 13:56:01 +0000
136@@ -18,10 +18,7 @@
137 import transaction
138 from zope.component import getUtility
139
140-from canonical.launchpad.helpers import (
141- emailPeople,
142- get_email_template,
143- )
144+from canonical.launchpad.helpers import get_email_template
145 from canonical.launchpad.scripts.logger import log
146 from canonical.launchpad.webapp import canonical_url
147 from lp.bugs.mail.bugnotificationbuilder import (
148@@ -30,6 +27,7 @@
149 )
150 from lp.bugs.mail.newbug import generate_bug_add_email
151 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
152+from lp.registry.model.person import get_recipients
153 from lp.services.mail.mailwrapper import MailWrapper
154
155
156@@ -108,7 +106,7 @@
157 # We will report this notification.
158 filtered_notifications.append(notification)
159 for recipient in notification.recipients:
160- email_people = emailPeople(recipient.person)
161+ email_people = get_recipients(recipient.person)
162 for email_person in email_people:
163 recipients_for_person = recipients.get(email_person)
164 if recipients_for_person is None:
165@@ -169,6 +167,7 @@
166 cached_filters = {}
167
168 bug_notification_set = getUtility(IBugNotificationSet)
169+
170 def get_filter_descriptions(recipients):
171 "Given a list of recipients, return the filter descriptions for them."
172 descriptions = set()
173
174=== modified file 'lib/lp/registry/model/person.py'
175--- lib/lp/registry/model/person.py 2011-03-29 03:13:40 +0000
176+++ lib/lp/registry/model/person.py 2011-03-31 13:56:01 +0000
177@@ -7,6 +7,7 @@
178
179 __metaclass__ = type
180 __all__ = [
181+ 'get_recipients',
182 'generate_nick',
183 'IrcID',
184 'IrcIDSet',
185@@ -57,6 +58,7 @@
186 And,
187 Desc,
188 Exists,
189+ In,
190 Join,
191 LeftJoin,
192 Min,
193@@ -4561,3 +4563,65 @@
194 if person is None:
195 raise ComponentLookupError()
196 return person
197+
198+
199+@ProxyFactory
200+def get_recipients(person):
201+ """Return a set of people who receive email for this Person (person/team).
202+
203+ If <person> has a preferred email, the set will contain only that
204+ person. If <person> doesn't have a preferred email but is a team,
205+ the set will contain the preferred email address of each member of
206+ <person>, including indirect members.
207+
208+ Finally, if <person> doesn't have a preferred email and is not a team,
209+ the set will be empty.
210+ """
211+ if person.preferredemail:
212+ return [person]
213+ elif person.is_team:
214+ # Get transitive members of team with a preferred email.
215+ return _get_recipients_for_team(person)
216+ else:
217+ return []
218+
219+
220+def _get_recipients_for_team(team):
221+ """Given a team without a preferred email, return recipients.
222+
223+ Helper for get_recipients, divided out simply to make get_recipients
224+ easier to understand in broad brush."""
225+ store = IStore(Person)
226+ source = store.using(TeamMembership,
227+ Join(Person,
228+ TeamMembership.personID==Person.id),
229+ LeftJoin(EmailAddress,
230+ EmailAddress.person == Person.id))
231+ pending_team_ids = [team.id]
232+ recipient_ids = set()
233+ seen = set()
234+ while pending_team_ids:
235+ intermediate_transitive_results = source.find(
236+ (TeamMembership.personID, EmailAddress.personID),
237+ In(TeamMembership.status,
238+ [TeamMembershipStatus.ADMIN.value,
239+ TeamMembershipStatus.APPROVED.value]),
240+ In(TeamMembership.teamID, pending_team_ids),
241+ Or(EmailAddress.status == EmailAddressStatus.PREFERRED,
242+ And(Not(Person.teamownerID == None),
243+ EmailAddress.status == None))).config(distinct=True)
244+ next_ids = []
245+ for (person_id,
246+ preferred_email_marker) in intermediate_transitive_results:
247+ if preferred_email_marker is None:
248+ # This is a team without a preferred email address.
249+ if person_id not in seen:
250+ next_ids.append(person_id)
251+ seen.add(person_id)
252+ else:
253+ recipient_ids.add(person_id)
254+ pending_team_ids = next_ids
255+ return getUtility(IPersonSet).getPrecachedPersonsFromIDs(
256+ recipient_ids,
257+ need_validity=True,
258+ need_preferred_email=True)
259
260=== modified file 'lib/lp/registry/tests/test_person.py'
261--- lib/lp/registry/tests/test_person.py 2011-03-26 16:28:39 +0000
262+++ lib/lp/registry/tests/test_person.py 2011-03-31 13:56:01 +0000
263@@ -35,6 +35,7 @@
264 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
265 from canonical.testing.layers import (
266 DatabaseFunctionalLayer,
267+ LaunchpadFunctionalLayer,
268 reconnect_stores,
269 )
270 from lp.answers.model.answercontact import AnswerContact
271@@ -61,7 +62,10 @@
272 KarmaCategory,
273 KarmaTotalCache,
274 )
275-from lp.registry.model.person import Person
276+from lp.registry.model.person import (
277+ get_recipients,
278+ Person,
279+ )
280 from lp.services.openid.model.openididentifier import OpenIdIdentifier
281 from lp.soyuz.enums import (
282 ArchivePurpose,
283@@ -1293,3 +1297,67 @@
284 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 3
285 # queries to the test.
286 self.assertThat(collector, HasQueryCount(LessThan(14)))
287+
288+
289+class TestGetRecipients(TestCaseWithFactory):
290+ """Tests for get_recipients"""
291+
292+ layer = LaunchpadFunctionalLayer
293+
294+ def setUp(self):
295+ super(TestGetRecipients, self).setUp()
296+ login('foo.bar@canonical.com')
297+
298+ def test_get_recipients_indirect(self):
299+ """Ensure get_recipients uses indirect memberships."""
300+ owner = self.factory.makePerson(
301+ displayname='Foo Bar', email='foo@bar.com', password='password')
302+ team = self.factory.makeTeam(owner)
303+ super_team = self.factory.makeTeam(team)
304+ recipients = get_recipients(super_team)
305+ self.assertEqual(set([owner]), set(recipients))
306+
307+ def test_get_recipients_team(self):
308+ """Ensure get_recipients uses teams with preferredemail."""
309+ owner = self.factory.makePerson(
310+ displayname='Foo Bar', email='foo@bar.com', password='password')
311+ team = self.factory.makeTeam(owner, email='team@bar.com')
312+ super_team = self.factory.makeTeam(team)
313+ recipients = get_recipients(super_team)
314+ self.assertEqual(set([team]), set(recipients))
315+
316+ def makePersonWithNoPreferredEmail(self, **kwargs):
317+ kwargs['email_address_status'] = EmailAddressStatus.NEW
318+ return self.factory.makePerson(**kwargs)
319+
320+ def get_test_recipients_person(self):
321+ person = self.factory.makePerson()
322+ recipients = get_recipients(person)
323+ self.assertEqual(set([person]), set(recipients))
324+
325+ def test_get_recipients_empty(self):
326+ """get_recipients returns empty set for person with no preferredemail.
327+ """
328+ recipients = get_recipients(self.makePersonWithNoPreferredEmail())
329+ self.assertEqual(set(), set(recipients))
330+
331+ def test_get_recipients_complex_indirect(self):
332+ """Ensure get_recipients uses indirect memberships."""
333+ owner = self.factory.makePerson(
334+ displayname='Foo Bar', email='foo@bar.com', password='password')
335+ team = self.factory.makeTeam(owner)
336+ super_team_member_person = self.factory.makePerson(
337+ displayname='Bing Bar', email='bing@bar.com')
338+ super_team_member_team = self.factory.makeTeam(
339+ email='baz@bar.com')
340+ super_team = self.factory.makeTeam(
341+ team, members=[super_team_member_person,
342+ super_team_member_team,
343+ self.makePersonWithNoPreferredEmail()])
344+ super_team_member_team.acceptInvitationToBeMemberOf(
345+ super_team, u'Go Team!')
346+ recipients = list(get_recipients(super_team))
347+ self.assertEqual(set([owner,
348+ super_team_member_person,
349+ super_team_member_team]),
350+ set(recipients))
351
352=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
353--- lib/lp/services/mail/notificationrecipientset.py 2010-12-02 16:13:51 +0000
354+++ lib/lp/services/mail/notificationrecipientset.py 2011-03-31 13:56:01 +0000
355@@ -14,7 +14,6 @@
356 from zope.interface import implements
357 from zope.security.proxy import isinstance as zope_isinstance
358
359-from canonical.launchpad.helpers import emailPeople
360 from canonical.launchpad.interfaces.launchpad import (
361 INotificationRecipientSet,
362 UnknownRecipientError,
363@@ -45,7 +44,7 @@
364 def getRecipients(self):
365 """See `INotificationRecipientSet`."""
366 return sorted(
367- self._personToRationale.keys(), key=attrgetter('displayname'))
368+ self._personToRationale.keys(), key=attrgetter('displayname'))
369
370 def getRecipientPersons(self):
371 """See `INotificationRecipientSet`."""
372@@ -88,6 +87,7 @@
373 def add(self, persons, reason, header):
374 """See `INotificationRecipientSet`."""
375 from zope.security.proxy import removeSecurityProxy
376+ from lp.registry.model.person import get_recipients
377 if IPerson.providedBy(persons):
378 persons = [persons]
379
380@@ -98,7 +98,7 @@
381 if person in self._personToRationale:
382 continue
383 self._personToRationale[person] = reason, header
384- for receiving_person in emailPeople(person):
385+ for receiving_person in get_recipients(person):
386 # Bypass zope's security because IEmailAddress.email is not
387 # public.
388 preferred_email = removeSecurityProxy(
389@@ -116,6 +116,7 @@
390 def remove(self, persons):
391 """See `INotificationRecipientSet`."""
392 from zope.security.proxy import removeSecurityProxy
393+ from lp.registry.model.person import get_recipients
394 if IPerson.providedBy(persons):
395 persons = [persons]
396 for person in persons:
397@@ -123,7 +124,7 @@
398 'You can only remove() an IPerson: %r' % person)
399 if person in self._personToRationale:
400 del self._personToRationale[person]
401- for removed_person in emailPeople(person):
402+ for removed_person in get_recipients(person):
403 # Bypass zope's security because IEmailAddress.email is
404 # not public.
405 preferred_email = removeSecurityProxy(
406
407=== modified file 'lib/lp/soyuz/scripts/ppareport.py'
408--- lib/lp/soyuz/scripts/ppareport.py 2010-08-31 11:11:09 +0000
409+++ lib/lp/soyuz/scripts/ppareport.py 2011-03-31 13:56:01 +0000
410@@ -20,9 +20,9 @@
411 from zope.component import getUtility
412
413 from canonical.config import config
414-from canonical.launchpad.helpers import emailPeople
415 from canonical.launchpad.webapp import canonical_url
416 from lp.registry.interfaces.distribution import IDistributionSet
417+from lp.registry.model.person import get_recipients
418 from lp.services.propertycache import cachedproperty
419 from lp.services.scripts.base import (
420 LaunchpadScript,
421@@ -181,7 +181,7 @@
422 self.output.write('= PPA user emails =\n')
423 people_to_email = set()
424 for ppa in self.ppas:
425- people_to_email.update(emailPeople(ppa.owner))
426+ people_to_email.update(get_recipients(ppa.owner))
427 sorted_people_to_email = sorted(
428 people_to_email, key=operator.attrgetter('name'))
429 for user in sorted_people_to_email: