Merge lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 11764
Proposed branch: lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout
Merge into: lp:launchpad
Diff against target: 185 lines (+66/-30)
2 files modified
lib/lp/registry/tests/test_person_vocabularies.py (+46/-0)
lib/lp/registry/vocabularies.py (+20/-30)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+38905@code.launchpad.net

Description of the change

Summary
-------

Fixes soft timeouts when the AJAX Picker searches the
ValidPersonOrTeamVocabulary or its subclasses.

Implementation details
----------------------

Rewrote query to remove private_inner_select subquery, which sped up the
query. Don't check the Account table, since the person record is
considered broken if the account status is active, and it doesn't have a
preferred email address. The EmailAddress is actually queried twice.
First, to search all VALIDATED and PREFERRED email addresses for certain
text. Secondly, to verify that any non-team has a PREFERRED email.
    lib/lp/registry/vocabularies.py

Added test to verify that the extra_clause is getting applied to the
query in _doSearch() when that method is passed in a search term.
Previously, the the picker would allow you to select a private team
as a member of itself. Later, it would fail because the __contains__()
method constructs a different query without a search term.
    lib/lp/registry/tests/test_person_vocabularies.py

Tests
-----

./bin/test -vv -t test_person_vocabularies

Demo and Q/A
------------

* Open http://launchpad.dev/~launchpad-beta-testers
  * Click on the "Add member" and search for "John Arbash Meinel".
  * It should not timeout.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thanks for working on this. I think this is good to land, but I have a one question/remark. I do not understand why the test is on the LaunchpadFunctionalLayer? I see database objects, but no library objects in the test. I think the test will run on the DatabaseFunctionalLayer.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/registry/tests/test_person_vocabularies.py'
--- lib/lp/registry/tests/test_person_vocabularies.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_person_vocabularies.py 2010-10-20 15:48:52 +0000
@@ -0,0 +1,46 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test the person vocabularies."""
5
6__metaclass__ = type
7
8from storm.store import Store
9from zope.schema.vocabulary import getVocabularyRegistry
10from zope.security.proxy import removeSecurityProxy
11
12from canonical.launchpad.ftests import login_person
13from canonical.testing.layers import DatabaseFunctionalLayer
14from lp.registry.interfaces.person import PersonVisibility
15from lp.testing import TestCaseWithFactory
16
17
18class TestValidTeamMemberVocabulary(TestCaseWithFactory):
19 """Test that the ValidTeamMemberVocabulary behaves as expected."""
20
21 layer = DatabaseFunctionalLayer
22
23 def searchVocabulary(self, team, text):
24 vocabulary_registry = getVocabularyRegistry()
25 naked_vocabulary = removeSecurityProxy(
26 vocabulary_registry.get(team, 'ValidTeamMember'))
27 return naked_vocabulary._doSearch(text)
28
29 def test_public_team_cannot_be_a_member_of_itself(self):
30 # A public team should be filtered by the vocab.extra_clause
31 # when provided a search term.
32 team_owner = self.factory.makePerson()
33 login_person(team_owner)
34 team = self.factory.makeTeam(owner=team_owner)
35 Store.of(team).flush()
36 self.assertFalse(team in self.searchVocabulary(team, team.name))
37
38 def test_private_team_cannot_be_a_member_of_itself(self):
39 # A private team should be filtered by the vocab.extra_clause
40 # when provided a search term.
41 team_owner = self.factory.makePerson()
42 login_person(team_owner)
43 team = self.factory.makeTeam(
44 owner=team_owner, visibility=PersonVisibility.PRIVATE)
45 Store.of(team).flush()
46 self.assertFalse(team in self.searchVocabulary(team, team.name))
047
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-09-27 20:47:58 +0000
+++ lib/lp/registry/vocabularies.py 2010-10-20 15:48:52 +0000
@@ -94,11 +94,9 @@
94 SQLBase,94 SQLBase,
95 sqlvalues,95 sqlvalues,
96 )96 )
97from canonical.launchpad.database.account import Account
98from canonical.launchpad.database.emailaddress import EmailAddress97from canonical.launchpad.database.emailaddress import EmailAddress
99from canonical.launchpad.database.stormsugar import StartsWith98from canonical.launchpad.database.stormsugar import StartsWith
100from canonical.launchpad.helpers import shortlist99from canonical.launchpad.helpers import shortlist
101from canonical.launchpad.interfaces.account import AccountStatus
102from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus100from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus
103from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities101from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
104from canonical.launchpad.interfaces.lpstorm import IStore102from canonical.launchpad.interfaces.lpstorm import IStore
@@ -109,7 +107,6 @@
109 IStoreSelector,107 IStoreSelector,
110 MAIN_STORE,108 MAIN_STORE,
111 )109 )
112from lp.app.browser.tales import DateTimeFormatterAPI
113from canonical.launchpad.webapp.vocabulary import (110from canonical.launchpad.webapp.vocabulary import (
114 BatchedCountableIterator,111 BatchedCountableIterator,
115 CountableIterator,112 CountableIterator,
@@ -118,6 +115,7 @@
118 NamedSQLObjectVocabulary,115 NamedSQLObjectVocabulary,
119 SQLObjectVocabularyBase,116 SQLObjectVocabularyBase,
120 )117 )
118from lp.app.browser.tales import DateTimeFormatterAPI
121from lp.blueprints.interfaces.specification import ISpecification119from lp.blueprints.interfaces.specification import ISpecification
122from lp.bugs.interfaces.bugtask import (120from lp.bugs.interfaces.bugtask import (
123 IBugTask,121 IBugTask,
@@ -524,16 +522,11 @@
524 # TeamParticipation table, which is very expensive. The search522 # TeamParticipation table, which is very expensive. The search
525 # for private teams does need that table but the number of private523 # for private teams does need that table but the number of private
526 # teams is very small so the cost is not great.524 # teams is very small so the cost is not great.
527 valid_email_statuses = (
528 EmailAddressStatus.VALIDATED,
529 EmailAddressStatus.PREFERRED,
530 )
531525
532 # First search for public persons and teams that match the text.526 # First search for public persons and teams that match the text.
533 public_tables = [527 public_tables = [
534 Person,528 Person,
535 LeftJoin(EmailAddress, EmailAddress.person == Person.id),529 LeftJoin(EmailAddress, EmailAddress.person == Person.id),
536 LeftJoin(Account, EmailAddress.account == Account.id),
537 ]530 ]
538531
539 # Create an inner query that will match public persons and teams532 # Create an inner query that will match public persons and teams
@@ -565,10 +558,15 @@
565 FROM Person, EmailAddress558 FROM Person, EmailAddress
566 WHERE EmailAddress.person = Person.id559 WHERE EmailAddress.person = Person.id
567 AND lower(email) LIKE ? || '%%'560 AND lower(email) LIKE ? || '%%'
561 AND EmailAddress.status IN (?, ?)
568 ) AS public_subquery562 ) AS public_subquery
569 ORDER BY rank DESC563 ORDER BY rank DESC
570 LIMIT ?564 LIMIT ?
571 """, (text, text, text, text, text, self.LIMIT))565 """, (text, text, text, text, text,
566 EmailAddressStatus.VALIDATED.value,
567 EmailAddressStatus.PREFERRED.value,
568 self.LIMIT))
569
572570
573 public_result = self.store.using(*public_tables).find(571 public_result = self.store.using(*public_tables).find(
574 Person,572 Person,
@@ -579,12 +577,9 @@
579 Or(# A valid person-or-team is either a team...577 Or(# A valid person-or-team is either a team...
580 # Note: 'Not' due to Bug 244768.578 # Note: 'Not' due to Bug 244768.
581 Not(Person.teamowner == None),579 Not(Person.teamowner == None),
582580 # Or a person who has a preferred email address.
583 # Or a person who has an active account and a working581 EmailAddress.status == EmailAddressStatus.PREFERRED),
584 # email address.582 ))
585 And(Account.status == AccountStatus.ACTIVE,
586 EmailAddress.status.is_in(valid_email_statuses))),
587 self.extra_clause))
588 # The public query doesn't need to be ordered as it will be done583 # The public query doesn't need to be ordered as it will be done
589 # at the end.584 # at the end.
590 public_result.order_by()585 public_result.order_by()
@@ -596,33 +591,28 @@
596 # Searching for private teams that match can be easier since we591 # Searching for private teams that match can be easier since we
597 # are only interested in teams. Teams can have email addresses592 # are only interested in teams. Teams can have email addresses
598 # but we're electing to ignore them here.593 # but we're electing to ignore them here.
599 private_inner_select = SQL("""
600 SELECT Person.id
601 FROM Person
602 WHERE Person.fti @@ ftq(?)
603 LIMIT ?
604 """, (text, self.LIMIT))
605 private_result = self.store.using(*private_tables).find(594 private_result = self.store.using(*private_tables).find(
606 Person,595 Person,
607 And(596 And(
608 Person.id.is_in(private_inner_select),597 SQL('Person.fti @@ ftq(?)', [text]),
609 private_query,598 private_query,
610 )599 )
611 )600 )
612601
613 # The private query doesn't need to be ordered as it will be done602 private_result.order_by(SQL('rank(fti, ftq(?)) DESC', [text]))
614 # at the end, and we need to eliminate the default ordering.603 private_result.config(limit=self.LIMIT)
615 private_result.order_by()
616604
617 combined_result = public_result.union(private_result)605 combined_result = public_result.union(private_result)
618 # Eliminate default ordering.606 # Eliminate default ordering.
619 combined_result.order_by()607 combined_result.order_by()
620 # XXX: BradCrittenden 2009-04-26 bug=217644: The use of Alias and608 # XXX: BradCrittenden 2009-04-26 bug=217644: The use of Alias and
621 # is a work-around for .count() not working with the 'distinct'609 # _get_select() is a work-around for .count() not working
622 # option.610 # with the 'distinct' option.
623 subselect = Alias(combined_result._get_select(), 'Person')611 subselect = Alias(combined_result._get_select(), 'Person')
624 exact_match = (Person.name == text)612 exact_match = (Person.name == text)
625 result = self.store.using(subselect).find((Person, exact_match))613 result = self.store.using(subselect).find(
614 (Person, exact_match),
615 self.extra_clause)
626 # XXX: BradCrittenden 2009-05-07 bug=373228: A bug in Storm prevents616 # XXX: BradCrittenden 2009-05-07 bug=373228: A bug in Storm prevents
627 # setting the 'distinct' and 'limit' options in a single call to617 # setting the 'distinct' and 'limit' options in a single call to
628 # .config(). The work-around is to split them up. Note the limit has618 # .config(). The work-around is to split them up. Note the limit has
@@ -755,8 +745,8 @@
755 Person.id NOT IN (745 Person.id NOT IN (
756 SELECT team FROM TeamParticipation746 SELECT team FROM TeamParticipation
757 WHERE person = %d747 WHERE person = %d
758 ) AND Person.id != %d748 )
759 """ % (self.team.id, self.team.id)749 """ % self.team.id
760750
761751
762class ValidTeamOwnerVocabulary(ValidPersonOrTeamVocabulary):752class ValidTeamOwnerVocabulary(ValidPersonOrTeamVocabulary):