launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03496
[Merge] lp:~adeuring/launchpad/bug-768443 into lp:launchpad
Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-768443 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #768443 in Launchpad itself: "ProductSeries:+index timeout with many releases"
https://bugs.launchpad.net/launchpad/+bug/768443
For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-768443/+merge/59782
This branch fixes bug 768443: ProductSeries:+index timeout with many releases
The reason for the timeout are many calls of Person.getAdministratedTeams() in lp.bugs.browser.structuralsubscription.expose_user_administered_teams_to_js() and lp.bugs.browser.structuralsubscription.expose_user_subscriptions_to_js()
These functions are called in lp.registry.browser.productseries.ProductSeriesView.initiakize() and in lp.registry.browser.milestone.MilestoneView.initialize()
One OOPS had more than 90 calls of getAdministratedTeams(); with an "SQL time" of 80..200msec per call this is an obvious cause for the timeouts.
At first I intended to cache the query result inside Person.getAdministratedTeams() but got scared when I noticed that this method is also used in security.py -- if the affected checkAuthenticated() method is called in a method where team memberships are changed, we might end with an invalid permission check, so I added the somewhat redundant cached property Person.adminstrated_teams which is now used in the affected functions expose_.*_to_js()
tests:
./bin/test bugs -vvt test_expose
./bin/test registry -vvt test_person.TestPersonTeams
no lint
--
https://code.launchpad.net/~adeuring/launchpad/bug-768443/+merge/59782
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-768443 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py 2011-04-26 17:28:37 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py 2011-05-03 14:11:18 +0000
@@ -427,7 +427,7 @@
api_request = IWebServiceClientRequest(request)
is_distro = IDistribution.providedBy(context)
if user is not None:
- administrated_teams = list(user.getAdministratedTeams())
+ administrated_teams = user.administrated_teams
if administrated_teams:
# Get this only if we need to.
membership = list(user.teams_participated_in)
@@ -460,7 +460,7 @@
if user is None:
administered_teams = []
else:
- administered_teams = user.getAdministratedTeams()
+ administered_teams = user.administrated_teams
if target is not None:
try:
=== modified file 'lib/lp/bugs/browser/tests/test_expose.py'
--- lib/lp/bugs/browser/tests/test_expose.py 2011-04-22 05:18:40 +0000
+++ lib/lp/bugs/browser/tests/test_expose.py 2011-05-03 14:11:18 +0000
@@ -32,6 +32,7 @@
from lp.testing import (
person_logged_in,
+ StormStatementRecorder,
TestCase,
TestCaseWithFactory,
)
@@ -56,8 +57,10 @@
class FakeUser:
"""A faux user that has a hard-coded set of administered teams."""
+ administrated_teams = [FakeTeam('Team One'), FakeTeam('Team Two')]
+
def getAdministratedTeams(self):
- return [FakeTeam('Team One'), FakeTeam('Team Two')]
+ return self.administrated_teams
def fake_absoluteURL(ob, request):
@@ -144,6 +147,34 @@
self.assertThat(team_info[0]['link'],
Equals('http://example.com/BugSupervisorSubTeam'))
+ def test_expose_user_administered_teams_to_js__uses_cached_teams(self):
+ # The function expose_user_administered_teams_to_js uses a
+ # cached list of administrated teams.
+ context = self.factory.makeProduct(owner=self.user)
+ self._setup_teams(self.user)
+
+ # The first call requires one query to retrieve the administrated
+ # teams.
+ with StormStatementRecorder() as recorder:
+ expose_user_administered_teams_to_js(
+ self.request, self.user, context,
+ absoluteURL=fake_absoluteURL)
+ statements_for_admininstrated_teams = [
+ statement for statement in recorder.statements
+ if statement.startswith("'SELECT *")]
+ self.assertEqual(1, len(statements_for_admininstrated_teams))
+
+ # Calling the function a second time does not require an
+ # SQL call to retrieve the administrated teams.
+ with StormStatementRecorder() as recorder:
+ expose_user_administered_teams_to_js(
+ self.request, self.user, context,
+ absoluteURL=fake_absoluteURL)
+ statements_for_admininstrated_teams = [
+ statement for statement in recorder.statements
+ if statement.startswith("'SELECT *")]
+ self.assertEqual(0, len(statements_for_admininstrated_teams))
+
def test_teams_owned_but_not_joined_are_not_included(self):
context = self.factory.makeProduct(owner=self.user)
team = self.factory.makeTeam(
@@ -333,3 +364,31 @@
self.assertEqual(
filter_info['subscriber_url'],
canonical_url(user, rootsite='mainsite'))
+
+ def test_expose_user_subscriptions_to_js__uses_cached_teams(self):
+ # The function expose_user_subscriptions_to_js() uses a
+ # cached list of administrated teams.
+ user = self.factory.makePerson()
+ target = self.factory.makeProduct()
+ request = LaunchpadTestRequest()
+ with person_logged_in(user):
+ sub = target.addBugSubscription(user, user)
+
+ # The first call requires one query to retrieve the administrated
+ # teams.
+ with StormStatementRecorder() as recorder:
+ expose_user_subscriptions_to_js(user, [sub], request)
+ statements_for_admininstrated_teams = [
+ statement for statement in recorder.statements
+ if statement.startswith("'SELECT *")]
+ self.assertEqual(1, len(statements_for_admininstrated_teams))
+
+ # Calling the function a second time does not require an
+ # SQL call to retrieve the administrated teams.
+ with person_logged_in(user):
+ with StormStatementRecorder() as recorder:
+ expose_user_subscriptions_to_js(user, [sub], request)
+ statements_for_admininstrated_teams = [
+ statement for statement in recorder.statements
+ if statement.startswith("'SELECT *")]
+ self.assertEqual(0, len(statements_for_admininstrated_teams))
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2011-04-26 13:57:15 +0000
+++ lib/lp/registry/interfaces/person.py 2011-05-03 14:11:18 +0000
@@ -979,6 +979,9 @@
title=_("Is this person due to be merged with another?"),
required=False, default=False))
+ administrated_teams = Attribute(
+ u"the teams that this person/team is an administrator of.")
+
@invariant
def personCannotHaveIcon(person):
"""Only Persons can have icons."""
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-04-27 14:51:54 +0000
+++ lib/lp/registry/model/person.py 2011-05-03 14:11:18 +0000
@@ -1562,6 +1562,10 @@
tm.setExpirationDate(expires, reviewer)
tm.setStatus(status, reviewer, comment=comment)
+ @cachedproperty
+ def administrated_teams(self):
+ return list(self.getAdministratedTeams())
+
def getAdministratedTeams(self):
"""See `IPerson`."""
owner_of_teams = Person.select('''
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-04-27 15:51:17 +0000
+++ lib/lp/registry/tests/test_person.py 2011-05-03 14:11:18 +0000
@@ -36,7 +36,6 @@
from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
from canonical.testing.layers import (
DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
reconnect_stores,
)
from lp.answers.model.answercontact import AnswerContact
@@ -269,6 +268,18 @@
retrieved_members = sorted(list(self.a_team.all_members_prepopulated))
self.assertEqual(expected_members, retrieved_members)
+ def test_administrated_teams(self):
+ # The property Person.administrated_teams is a cached copy of
+ # the result of Person.getAdministratedTeams().
+ expected = [self.b_team, self.c_team]
+ self.assertEqual(expected, list(self.user.getAdministratedTeams()))
+ with StormStatementRecorder() as recorder:
+ self.assertEqual(expected, self.user.administrated_teams)
+ self.user.administrated_teams
+ # The second access of administrated_teams did not require an
+ # SQL query, hence the total number of SQL queries is 1.
+ self.assertEqual(1, len(recorder.queries))
+
class TestPerson(TestCaseWithFactory):