Merge lp:~edwin-grubbs/launchpad/bug-682727-bug-count-timeout into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 12158
Proposed branch: lp:~edwin-grubbs/launchpad/bug-682727-bug-count-timeout
Merge into: lp:launchpad
Diff against target: 200 lines (+125/-9)
3 files modified
lib/lp/bugs/model/bugtask.py (+7/-3)
lib/lp/bugs/tests/test_bugtaskset.py (+91/-0)
lib/lp/testing/factory.py (+27/-6)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-682727-bug-count-timeout
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+45141@code.launchpad.net

Commit message

[r=gmb][ui=none][bug=682727] Fix timeout on $project/+series page by simplifying bug count query.

Description of the change

Summary
-------

This branch fixes a timeout on the $project/+series page.

I simplified the permission query for bug counts on milestones, since
the query which determines which private bugs a user can view is costly,
and displaying a count including all private bugs doesn't reveal any
sensitive information.

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

configs/testrunner/launchpad.conf
lib/lp/bugs/model/bugtask.py
lib/lp/bugs/tests/test_bugtaskset.py
lib/lp/testing/factory.py

Tests
-----

./bin/test -vvp lp.bugs.tests.test_bugtaskset

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

* Open https://code.qastaging.launchpad.net/obsolete-junk/+series
  * It should not timeout.

Lint
----

No lint.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Edwin,

Couple of minor nitpicks here, but otherwise this is r=me.

60 + def get_counts(self, user):
61 + counts = self.bugtask_set.getStatusCountsForProductSeries(
62 + user, self.series)
63 + return [(BugTaskStatus.items[status_id], count)
64 + for status_id, count in counts]

The indentation for these statements is a bit weird. I think that it
should look like this:

    counts = self.bugtask_set.getStatusCountsForProductSeries(
        user, self.series)
    return [
        (BugTaskStatus.items[status_id], count)
        for status_id, count in counts]

186 + [bugtask] = bug.bugtasks

I know that the context makes it unlikely that we'll have > 1 bugtask on
the bug at this point, but for safety's (and readability's) sake I'd
prefer this to be:

    bugtask = bug.default_bugtask

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/model/bugtask.py'
2--- lib/lp/bugs/model/bugtask.py 2010-12-23 11:35:12 +0000
3+++ lib/lp/bugs/model/bugtask.py 2011-01-04 17:02:31 +0000
4@@ -2485,9 +2485,13 @@
5
6 def getStatusCountsForProductSeries(self, user, product_series):
7 """See `IBugTaskSet`."""
8- bug_privacy_filter = get_bug_privacy_filter(user)
9- if bug_privacy_filter != "":
10- bug_privacy_filter = 'AND ' + bug_privacy_filter
11+ if user is None:
12+ bug_privacy_filter = 'AND Bug.private = FALSE'
13+ else:
14+ # Since the count won't reveal sensitive information, and
15+ # since the get_bug_privacy_filter() check for non-admins is
16+ # costly, don't filter those bugs at all.
17+ bug_privacy_filter = ''
18 cur = cursor()
19
20 # The union is actually much faster than a LEFT JOIN with the
21
22=== added file 'lib/lp/bugs/tests/test_bugtaskset.py'
23--- lib/lp/bugs/tests/test_bugtaskset.py 1970-01-01 00:00:00 +0000
24+++ lib/lp/bugs/tests/test_bugtaskset.py 2011-01-04 17:02:31 +0000
25@@ -0,0 +1,91 @@
26+# Copyright 2011 Canonical Ltd. This software is licensed under the
27+# GNU Affero General Public License version 3 (see the file LICENSE).
28+
29+"""Tests for BugTaskSet."""
30+
31+__metaclass__ = type
32+
33+from zope.component import getUtility
34+
35+from canonical.testing.layers import DatabaseFunctionalLayer
36+from lp.bugs.interfaces.bugtask import (
37+ BugTaskStatus,
38+ IBugTaskSet,
39+ )
40+from lp.testing import (
41+ login_person,
42+ TestCaseWithFactory,
43+ )
44+
45+
46+class TestStatusCountsForProductSeries(TestCaseWithFactory):
47+ """Test BugTaskSet.getStatusCountsForProductSeries()."""
48+
49+ layer = DatabaseFunctionalLayer
50+
51+ def setUp(self):
52+ super(TestStatusCountsForProductSeries, self).setUp()
53+ self.bugtask_set = getUtility(IBugTaskSet)
54+ self.owner = self.factory.makePerson()
55+ login_person(self.owner)
56+ self.product = self.factory.makeProduct(owner=self.owner)
57+ self.series = self.factory.makeProductSeries(product=self.product)
58+ self.milestone = self.factory.makeMilestone(productseries=self.series)
59+
60+ def get_counts(self, user):
61+ counts = self.bugtask_set.getStatusCountsForProductSeries(
62+ user, self.series)
63+ return [
64+ (BugTaskStatus.items[status_id], count)
65+ for status_id, count in counts]
66+
67+ def test_privacy_and_counts_for_unauthenticated_user(self):
68+ # An unauthenticated user should see bug counts for each status
69+ # that do not include private bugs.
70+ self.factory.makeBug(milestone=self.milestone)
71+ self.factory.makeBug(milestone=self.milestone, private=True)
72+ self.factory.makeBug(series=self.series)
73+ self.factory.makeBug(series=self.series, private=True)
74+ self.assertEqual(
75+ [(BugTaskStatus.NEW, 2)],
76+ self.get_counts(None))
77+
78+ def test_privacy_and_counts_for_owner(self):
79+ # The owner should see bug counts for each status that do
80+ # include all private bugs.
81+ self.factory.makeBug(milestone=self.milestone)
82+ self.factory.makeBug(milestone=self.milestone, private=True)
83+ self.factory.makeBug(series=self.series)
84+ self.factory.makeBug(series=self.series, private=True)
85+ self.assertEqual(
86+ [(BugTaskStatus.NEW, 4)],
87+ self.get_counts(self.owner))
88+
89+ def test_privacy_and_counts_for_other_user(self):
90+ # A random authenticated user should see bug counts for each
91+ # status that do include all private bugs, since it is costly to
92+ # query just the private bugs that the user has access to view,
93+ # and this query may be run many times on a single page.
94+ self.factory.makeBug(milestone=self.milestone)
95+ self.factory.makeBug(milestone=self.milestone, private=True)
96+ self.factory.makeBug(series=self.series)
97+ self.factory.makeBug(series=self.series, private=True)
98+ other = self.factory.makePerson()
99+ self.assertEqual(
100+ [(BugTaskStatus.NEW, 4)],
101+ self.get_counts(other))
102+
103+ def test_multiple_statuses(self):
104+ # Test that separate counts are provided for each status that
105+ # bugs are found in.
106+ for status in (BugTaskStatus.INVALID, BugTaskStatus.OPINION):
107+ self.factory.makeBug(milestone=self.milestone, status=status)
108+ self.factory.makeBug(series=self.series, status=status)
109+ for i in range(3):
110+ self.factory.makeBug(series=self.series)
111+ self.assertEqual(
112+ [(BugTaskStatus.INVALID, 2),
113+ (BugTaskStatus.OPINION, 2),
114+ (BugTaskStatus.NEW, 3),
115+ ],
116+ self.get_counts(None))
117
118=== modified file 'lib/lp/testing/factory.py'
119--- lib/lp/testing/factory.py 2010-12-24 10:03:15 +0000
120+++ lib/lp/testing/factory.py 2011-01-04 17:02:31 +0000
121@@ -7,7 +7,6 @@
122
123 This module should not contain tests (but it should be tested).
124 """
125-from lp.code.model.recipebuild import RecipeBuildRecord
126
127 __metaclass__ = type
128 __all__ = [
129@@ -161,6 +160,7 @@
130 PreviewDiff,
131 StaticDiff,
132 )
133+from lp.code.model.recipebuild import RecipeBuildRecord
134 from lp.codehosting.codeimport.worker import CodeImportSourceDetails
135 from lp.hardwaredb.interfaces.hwdb import (
136 HWSubmissionFormat,
137@@ -1371,26 +1371,41 @@
138 def makeBug(self, product=None, owner=None, bug_watch_url=None,
139 private=False, date_closed=None, title=None,
140 date_created=None, description=None, comment=None,
141- status=None, distribution=None):
142+ status=None, distribution=None, milestone=None, series=None):
143 """Create and return a new, arbitrary Bug.
144
145 The bug returned uses default values where possible. See
146 `IBugSet.new` for more information.
147
148 :param product: If the product is not set, and if the parameter
149- distribution is not set, a product is created and this is
150- used as the primary bug target.
151+ distribution, milestone, and series are not set, a product
152+ is created and this is used as the primary bug target.
153 :param owner: The reporter of the bug. If not set, one is created.
154 :param bug_watch_url: If specified, create a bug watch pointing
155 to this URL.
156 :param distribution: If set, the distribution is used as the
157 default bug target.
158+ :param milestone: If set, the milestone.target must match the product
159+ or distribution parameters, or the those parameters must be None.
160+ :param series: If set, the series.product must match the product
161+ parameter, or the series.distribution must match the distribution
162+ parameter, or the those parameters must be None.
163
164 At least one of the parameters distribution and product must be
165 None, otherwise, an assertion error will be raised.
166 """
167 if product is None and distribution is None:
168- product = self.makeProduct()
169+ if milestone is not None:
170+ # One of these will be None.
171+ product = milestone.product
172+ distribution = milestone.distribution
173+ elif series is not None:
174+ if IProductSeries.providedBy(series):
175+ product = series.product
176+ else:
177+ distribution = series.distribution
178+ else:
179+ product = self.makeProduct()
180 if owner is None:
181 owner = self.makePerson()
182 if title is None:
183@@ -1407,10 +1422,16 @@
184 if bug_watch_url is not None:
185 # fromText() creates a bug watch associated with the bug.
186 getUtility(IBugWatchSet).fromText(bug_watch_url, bug, owner)
187+ bugtask = bug.default_bugtask
188 if date_closed is not None:
189- [bugtask] = bug.bugtasks
190 bugtask.transitionToStatus(
191 BugTaskStatus.FIXRELEASED, owner, when=date_closed)
192+ if milestone is not None:
193+ bugtask.transitionToMilestone(milestone, milestone.target.owner)
194+ else:
195+ task = bug.addTask(owner, series)
196+ task.transitionToStatus(status, owner)
197+
198 return bug
199
200 def makeBugTask(self, bug=None, target=None, owner=None):