Merge lp:~wgrant/launchpad/bug-1500576 into lp:launchpad

Proposed by William Grant on 2015-09-29
Status: Merged
Merged at revision: 17770
Proposed branch: lp:~wgrant/launchpad/bug-1500576
Merge into: lp:launchpad
Diff against target: 149 lines (+60/-4)
4 files modified
lib/lp/_schema_circular_imports.py (+1/-1)
lib/lp/code/interfaces/branch.py (+6/-2)
lib/lp/code/model/branch.py (+5/-0)
lib/lp/code/tests/test_branch_webservice.py (+48/-1)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-1500576
Reviewer Review Type Date Requested Status
Colin Watson 2015-09-29 Approve on 2015-09-29
Review via email: mp+272676@code.launchpad.net

Commit Message

Precache branch permissions in Branch.landing_candidates, fixing API timeouts.

Description of the Change

Fix (or at least markedly reduce) API timeouts of Branch.landing_candidates (bug #1500576). OOPS-99645f480967e3ac2b47509394004884 shows that nearly 500 of the 600 queries are checking branch privacy, so I replaced the property with a call to getPrecachedLandingCandidates. It means using LaunchBag in the model, but it's the only option here due to lazr.restful limitations.

There are three queries per MP left over, but they're comparatively few and cheap. A test is in place to ensure it doesn't regress past those.

landing_targets and dependent_branches could arguably use the same treatment, but they're very rarely used and almost always just a couple of entries long.

To post a comment you must log in.
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2015-08-03 11:45:43 +0000
3+++ lib/lp/_schema_circular_imports.py 2015-09-29 00:49:05 +0000
4@@ -238,7 +238,7 @@
5 patch_collection_property(IBranch, 'linked_bugs', IBug)
6 patch_collection_property(IBranch, 'dependent_branches', IBranchMergeProposal)
7 patch_entry_return_type(IBranch, 'getSubscription', IBranchSubscription)
8-patch_collection_property(IBranch, 'landing_candidates', IBranchMergeProposal)
9+patch_collection_property(IBranch, '_api_landing_candidates', IBranchMergeProposal)
10 patch_collection_property(IBranch, 'landing_targets', IBranchMergeProposal)
11 patch_plain_parameter_type(IBranch, 'linkBug', 'bug', IBug)
12 patch_plain_parameter_type(
13
14=== modified file 'lib/lp/code/interfaces/branch.py'
15--- lib/lp/code/interfaces/branch.py 2015-09-23 15:38:13 +0000
16+++ lib/lp/code/interfaces/branch.py 2015-09-29 00:49:05 +0000
17@@ -551,14 +551,18 @@
18 'the source branch.'),
19 readonly=True,
20 value_type=Reference(Interface)))
21- landing_candidates = exported(
22+ landing_candidates = Attribute(
23+ 'A collection of the merge proposals where this branch is '
24+ 'the target branch.')
25+ _api_landing_candidates = exported(
26 CollectionField(
27 title=_('Landing Candidates'),
28 description=_(
29 'A collection of the merge proposals where this branch is '
30 'the target branch.'),
31 readonly=True,
32- value_type=Reference(Interface)))
33+ value_type=Reference(Interface)),
34+ exported_as='landing_candidates')
35 dependent_branches = exported(
36 CollectionField(
37 title=_('Dependent Branches'),
38
39=== modified file 'lib/lp/code/model/branch.py'
40--- lib/lp/code/model/branch.py 2015-09-23 15:38:13 +0000
41+++ lib/lp/code/model/branch.py 2015-09-29 00:49:05 +0000
42@@ -179,6 +179,7 @@
43 from lp.services.propertycache import cachedproperty
44 from lp.services.webapp import urlappend
45 from lp.services.webapp.authorization import check_permission
46+from lp.services.webapp.interfaces import ILaunchBag
47
48
49 @implementer(IBranch, IPrivacy, IInformationType)
50@@ -494,6 +495,10 @@
51 self.landing_candidates, pre_iter_hook=eager_load)
52
53 @property
54+ def _api_landing_candidates(self):
55+ return self.getPrecachedLandingCandidates(getUtility(ILaunchBag).user)
56+
57+ @property
58 def dependent_branches(self):
59 """See `IBranch`."""
60 return BranchMergeProposal.select("""
61
62=== modified file 'lib/lp/code/tests/test_branch_webservice.py'
63--- lib/lp/code/tests/test_branch_webservice.py 2012-09-26 07:59:35 +0000
64+++ lib/lp/code/tests/test_branch_webservice.py 2015-09-29 00:49:05 +0000
65@@ -4,6 +4,10 @@
66 __metaclass__ = type
67
68 from lazr.restfulclient.errors import BadRequest
69+from testtools.matchers import (
70+ Equals,
71+ LessThan,
72+ )
73 from zope.component import getUtility
74 from zope.security.management import endInteraction
75 from zope.security.proxy import removeSecurityProxy
76@@ -12,22 +16,64 @@
77 from lp.code.interfaces.branch import IBranchSet
78 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
79 from lp.registry.interfaces.pocket import PackagePublishingPocket
80+from lp.services.webapp.interfaces import OAuthPermission
81 from lp.testing import (
82+ admin_logged_in,
83 api_url,
84 launchpadlib_for,
85 login_person,
86 logout,
87+ record_two_runs,
88 run_with_login,
89 TestCaseWithFactory,
90 )
91 from lp.testing.layers import DatabaseFunctionalLayer
92+from lp.testing.pages import webservice_for_person
93+from lp.testing.matchers import HasQueryCount
94+
95+
96+class TestBranch(TestCaseWithFactory):
97+
98+ layer = DatabaseFunctionalLayer
99+
100+ def test_landing_candidates_constant_queries(self):
101+ target = self.factory.makeProduct()
102+ login_person(target.owner)
103+ trunk = self.factory.makeBranch(target=target)
104+ trunk_url = api_url(trunk)
105+ webservice = webservice_for_person(
106+ target.owner, permission=OAuthPermission.WRITE_PRIVATE)
107+ logout()
108+
109+ def create_mp():
110+ with admin_logged_in():
111+ branch = self.factory.makeBranch(
112+ target=target,
113+ stacked_on=self.factory.makeBranch(
114+ target=target,
115+ information_type=InformationType.PRIVATESECURITY),
116+ information_type=InformationType.PRIVATESECURITY)
117+ self.factory.makeBranchMergeProposal(
118+ source_branch=branch, target_branch=trunk)
119+
120+ def list_mps():
121+ webservice.get(trunk_url + '/landing_candidates')
122+
123+ list_mps()
124+ recorder1, recorder2 = record_two_runs(list_mps, create_mp, 2)
125+ self.assertThat(recorder1, HasQueryCount(LessThan(30)))
126+ # Each new MP triggers a query for Person, BranchMergeProposal
127+ # and PreviewDiff. Eventually they should be batched, but this
128+ # at least ensures the situation doesn't get worse.
129+ self.assertThat(
130+ recorder2, HasQueryCount(Equals(recorder1.count + (2 * 3))))
131
132
133 class TestBranchOperations(TestCaseWithFactory):
134
135 layer = DatabaseFunctionalLayer
136
137- def test_createMergeProposal_fails_if_reviewers_and_review_types_are_different_sizes(self):
138+ def test_createMergeProposal_fails_if_reviewers_and_types_mismatch(self):
139
140 source = self.factory.makeBranch(name='rock')
141 source_url = api_url(source)
142@@ -105,6 +151,7 @@
143 self.assertEqual(
144 '~barney/myproduct/mybranch', renamed_branch.unique_name)
145
146+
147 class TestBranchDeletes(TestCaseWithFactory):
148
149 layer = DatabaseFunctionalLayer