Merge lp:~abentley/launchpad/fix-bp-timeouts into lp:launchpad

Proposed by Aaron Bentley on 2012-11-16
Status: Merged
Approved by: j.c.sackett on 2012-11-16
Approved revision: no longer in the source branch.
Merged at revision: 16286
Proposed branch: lp:~abentley/launchpad/fix-bp-timeouts
Merge into: lp:launchpad
Diff against target: 449 lines (+68/-115)
9 files modified
lib/lp/blueprints/model/specification.py (+3/-64)
lib/lp/blueprints/model/specificationworkitem.py (+6/-5)
lib/lp/blueprints/model/sprint.py (+1/-2)
lib/lp/blueprints/tests/test_specification.py (+18/-16)
lib/lp/registry/model/distribution.py (+2/-1)
lib/lp/registry/model/milestone.py (+6/-6)
lib/lp/registry/model/person.py (+16/-12)
lib/lp/registry/model/product.py (+7/-6)
lib/lp/registry/model/sharingjob.py (+9/-3)
To merge this branch: bzr merge lp:~abentley/launchpad/fix-bp-timeouts
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-11-16 Approve on 2012-11-16
Review via email: mp+134744@code.launchpad.net

Commit Message

Fix timeouts checking blueprint visiblity

Description of the Change

= Summary =
Fix the following bugs:
Bug #1075569: Timeout loading milestone because of blueprints
Bug #1077980: viewing project page leads to timeout when private blueprints exists
Bug #1078239: loading team blueprints leads to timeout Person:+index
Bug #1079390: Person:+upcomingwork timeout querying specifiations

== Proposed fix ==
Switch from get_specification_privacy_filter to visible_specification_query everywhere.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of private products.

== Implementation details ==
visible_specification_query does not guarantee distinct results, so added config(distinct=True) as needed.

Since get_specification_privacy_filter is no longer used, it is deleted, and its tests migrated to visible_specification_query.

TestPersonUpcomingWork.test_container_progressbar assumed that WorkItem containers were in a specific order, based on the id of their initial WorkItem. This work changed the ordering, but a consistent ordering makes sense, so I've made the ordering explicit.

All the call sites of get_specification_filters now use visible_specification_query, so its assume_product_active behaviour can now become mandatory.

_preload_specifications_people is used by both Product and Distribution, so tweaked Distribution to supply tables.

In sharingjob, replaced get_specification_privacy_filter by introducing a sub-select, because otherwise, negating visible_specification_query would be very tricky.

Fixed lint in SpecificationWorkItem.__init__.

Fixed long line by assigning LatestPersonSourcePackageReleaseCache.upload_distroseries_id to a local variable.

== Tests ==
bin/test -v

== Demo and Q/A ==
Go to the staging versions of all pages linked in the bugs and see whether they consistently time out.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/model/specificationworkitem.py
  lib/lp/registry/model/sharingjob.py
  lib/lp/blueprints/tests/test_specification.py
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/model/sprint.py
  lib/lp/registry/model/product.py
  lib/lp/registry/model/person.py
  lib/lp/registry/model/milestone.py
  lib/lp/registry/model/distribution.py

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

Looks good, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/model/specification.py'
2--- lib/lp/blueprints/model/specification.py 2012-11-12 16:40:28 +0000
3+++ lib/lp/blueprints/model/specification.py 2012-11-16 20:58:25 +0000
4@@ -4,7 +4,6 @@
5 __metaclass__ = type
6 __all__ = [
7 'get_specification_filters',
8- 'get_specification_privacy_filter',
9 'HasSpecificationsMixin',
10 'recursive_blocked_query',
11 'recursive_dependent_query',
12@@ -960,7 +959,7 @@
13 elif sort == SpecificationSort.DATE:
14 return (Desc(Specification.datecreated), Specification.id)
15
16- def _preload_specifications_people(self, clauses):
17+ def _preload_specifications_people(self, tables, clauses):
18 """Perform eager loading of people and their validity for query.
19
20 :param query: a string query generated in the 'specifications'
21@@ -1001,7 +1000,7 @@
22 index += 1
23 decorator(person, column)
24
25- results = Store.of(self).find(Specification, *clauses)
26+ results = Store.of(self).using(*tables).find(Specification, *clauses)
27 return DecoratedResultSet(results, pre_iter_hook=cache_people)
28
29 @property
30@@ -1230,58 +1229,6 @@
31 return Specification.get(spec_id)
32
33
34-def get_specification_privacy_filter(user):
35- """Return a Storm expression for filtering specifications by privacy.
36-
37- :param user: A Person ID or a column reference.
38- :return: A Storm expression to check if a peron has access grants
39- for a specification.
40- """
41- # Avoid circular imports.
42- from lp.registry.model.accesspolicy import (
43- AccessArtifact,
44- AccessPolicy,
45- AccessPolicyGrantFlat,
46- )
47- public_specification_filter = (
48- Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES))
49- if user is None:
50- return public_specification_filter
51- return Or(
52- public_specification_filter,
53- Specification.id.is_in(
54- Select(
55- Specification.id,
56- tables=(
57- Specification,
58- Join(
59- AccessPolicy,
60- And(
61- Or(
62- Specification.productID ==
63- AccessPolicy.product_id,
64- Specification.distributionID ==
65- AccessPolicy.distribution_id),
66- Specification.information_type ==
67- AccessPolicy.type)),
68- Join(
69- AccessPolicyGrantFlat,
70- AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
71- LeftJoin(
72- AccessArtifact,
73- AccessPolicyGrantFlat.abstract_artifact_id ==
74- AccessArtifact.id),
75- Join(
76- TeamParticipation,
77- And(
78- TeamParticipation.team ==
79- AccessPolicyGrantFlat.grantee_id,
80- TeamParticipation.person == user))),
81- where=Or(
82- AccessPolicyGrantFlat.abstract_artifact_id == None,
83- AccessArtifact.specification_id == Specification.id))))
84-
85-
86 def visible_specification_query(user):
87 """Return a Storm expression and list of tables for filtering
88 specifications by privacy.
89@@ -1324,21 +1271,13 @@
90 return tables, clauses
91
92
93-def get_specification_filters(filter, assume_product_active=False):
94+def get_specification_filters(filter):
95 """Return a list of Storm expressions for filtering Specifications.
96
97 :param filters: A collection of SpecificationFilter and/or strings.
98 Strings are used for text searches.
99- :param assume_product_active: If True, assume the Product is active,
100- instead of ensuring it is active.
101 """
102- from lp.registry.model.product import Product
103 clauses = []
104- # If Product is used, it must be active.
105- if not assume_product_active:
106- clauses.extend([Or(Specification.product == None,
107- Not(Specification.productID.is_in(Select(Product.id,
108- Product.active == False))))])
109 # ALL is the trump card.
110 if SpecificationFilter.ALL in filter:
111 return clauses
112
113=== modified file 'lib/lp/blueprints/model/specificationworkitem.py'
114--- lib/lp/blueprints/model/specificationworkitem.py 2012-04-05 13:05:04 +0000
115+++ lib/lp/blueprints/model/specificationworkitem.py 2012-11-16 20:58:25 +0000
116@@ -29,6 +29,7 @@
117 implements(ISpecificationWorkItem)
118
119 __storm_table__ = 'SpecificationWorkItem'
120+ __storm_order__ = 'id'
121
122 id = Int(primary=True)
123 title = Unicode(allow_none=False)
124@@ -53,12 +54,12 @@
125
126 def __init__(self, title, status, specification, assignee, milestone,
127 sequence):
128- self.title=title
129- self.status=status
130+ self.title = title
131+ self.status = status
132 self.specification = specification
133- self.assignee=assignee
134- self.milestone=milestone
135- self.sequence=sequence
136+ self.assignee = assignee
137+ self.milestone = milestone
138+ self.sequence = sequence
139
140 @property
141 def is_complete(self):
142
143=== modified file 'lib/lp/blueprints/model/sprint.py'
144--- lib/lp/blueprints/model/sprint.py 2012-11-12 16:40:28 +0000
145+++ lib/lp/blueprints/model/sprint.py 2012-11-16 20:58:25 +0000
146@@ -155,8 +155,7 @@
147 if len(statuses) > 0:
148 query.append(Or(*statuses))
149 # Filter for specification text
150- query.extend(
151- get_specification_filters(filter, assume_product_active=True))
152+ query.extend(get_specification_filters(filter))
153 return tables, query
154
155 def all_specifications(self, user):
156
157=== modified file 'lib/lp/blueprints/tests/test_specification.py'
158--- lib/lp/blueprints/tests/test_specification.py 2012-10-16 14:47:54 +0000
159+++ lib/lp/blueprints/tests/test_specification.py 2012-11-16 20:58:25 +0000
160@@ -33,8 +33,8 @@
161 from lp.blueprints.errors import TargetAlreadyHasSpecification
162 from lp.blueprints.interfaces.specification import ISpecificationSet
163 from lp.blueprints.model.specification import (
164- get_specification_privacy_filter,
165 Specification,
166+ visible_specification_query,
167 )
168 from lp.registry.enums import (
169 SharingPermission,
170@@ -397,8 +397,8 @@
171 specification.target.owner, specification,
172 error_expected=False, attribute='name', value='foo')
173
174- def test_get_specification_privacy_filter(self):
175- # get_specification_privacy_filter() returns a Storm expression
176+ def test_visible_specification_query(self):
177+ # visible_specification_query returns a Storm expression
178 # that can be used to filter specifications by their visibility-
179 owner = self.factory.makePerson()
180 product = self.factory.makeProduct(
181@@ -413,30 +413,32 @@
182 all_specs = [
183 public_spec, proprietary_spec_1, proprietary_spec_2]
184 store = Store.of(product)
185- specs_for_anon = store.find(
186- Specification, get_specification_privacy_filter(None),
187- Specification.productID == product.id)
188- self.assertContentEqual([public_spec], specs_for_anon)
189+ tables, query = visible_specification_query(None)
190+ specs_for_anon = store.using(*tables).find(
191+ Specification,
192+ Specification.productID == product.id, *query)
193+ self.assertContentEqual([public_spec],
194+ specs_for_anon.config(distinct=True))
195 # Product owners havae grants on the product, the privacy
196 # filter returns thus all specifications for them.
197- specs_for_owner = store.find(
198- Specification, get_specification_privacy_filter(owner.id),
199- Specification.productID == product.id)
200+ tables, query = visible_specification_query(owner.id)
201+ specs_for_owner = store.using(*tables).find(
202+ Specification, Specification.productID == product.id, *query)
203 self.assertContentEqual(all_specs, specs_for_owner)
204 # The filter returns only public specs for ordinary users.
205 user = self.factory.makePerson()
206- specs_for_other_user = store.find(
207- Specification, get_specification_privacy_filter(user.id),
208- Specification.productID == product.id)
209+ tables, query = visible_specification_query(user.id)
210+ specs_for_other_user = store.using(*tables).find(
211+ Specification, Specification.productID == product.id, *query)
212 self.assertContentEqual([public_spec], specs_for_other_user)
213 # If the user has a grant for a specification, the filter returns
214 # this specification too.
215 with person_logged_in(owner):
216 getUtility(IService, 'sharing').ensureAccessGrants(
217 [user], owner, specifications=[proprietary_spec_1])
218- specs_for_other_user = store.find(
219- Specification, get_specification_privacy_filter(user.id),
220- Specification.productID == product.id)
221+ tables, query = visible_specification_query(user.id)
222+ specs_for_other_user = store.using(*tables).find(
223+ Specification, Specification.productID == product.id, *query)
224 self.assertContentEqual(
225 [public_spec, proprietary_spec_1], specs_for_other_user)
226
227
228=== modified file 'lib/lp/registry/model/distribution.py'
229--- lib/lp/registry/model/distribution.py 2012-11-02 00:53:58 +0000
230+++ lib/lp/registry/model/distribution.py 2012-11-16 20:58:25 +0000
231@@ -952,7 +952,8 @@
232 constraint)
233
234 if prejoin_people:
235- results = self._preload_specifications_people(query)
236+ results = self._preload_specifications_people([Specification],
237+ query)
238 else:
239 results = Store.of(self).find(
240 Specification,
241
242=== modified file 'lib/lp/registry/model/milestone.py'
243--- lib/lp/registry/model/milestone.py 2012-10-26 08:59:24 +0000
244+++ lib/lp/registry/model/milestone.py 2012-11-16 20:58:25 +0000
245@@ -39,8 +39,8 @@
246
247 from lp.app.errors import NotFoundError
248 from lp.blueprints.model.specification import (
249- get_specification_privacy_filter,
250 Specification,
251+ visible_specification_query
252 )
253 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
254 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
255@@ -155,10 +155,10 @@
256 """See `IMilestoneData`"""
257 from lp.registry.model.person import Person
258 store = Store.of(self.target)
259- origin = [
260- Specification,
261+ origin, clauses = visible_specification_query(user)
262+ origin.extend([
263 LeftJoin(Person, Specification.assigneeID == Person.id),
264- ]
265+ ])
266 milestones = self._milestone_ids_expr(user)
267
268 results = store.using(*origin).find(
269@@ -176,12 +176,12 @@
270 milestones),
271 SpecificationWorkItem.deleted == False)),
272 all=True)),
273- get_specification_privacy_filter(user))
274- results.config(distinct=True)
275+ *clauses)
276 ordered_results = results.order_by(Desc(Specification.priority),
277 Specification.definition_status,
278 Specification.implementation_status,
279 Specification.title)
280+ ordered_results.config(distinct=True)
281 mapper = lambda row: row[0]
282 return DecoratedResultSet(ordered_results, mapper)
283
284
285=== modified file 'lib/lp/registry/model/person.py'
286--- lib/lp/registry/model/person.py 2012-11-14 02:03:32 +0000
287+++ lib/lp/registry/model/person.py 2012-11-16 20:58:25 +0000
288@@ -130,10 +130,10 @@
289 )
290 from lp.blueprints.model.specification import (
291 get_specification_filters,
292- get_specification_privacy_filter,
293 HasSpecificationsMixin,
294 Specification,
295 spec_started_clause,
296+ visible_specification_query,
297 )
298 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
299 from lp.bugs.interfaces.bugtarget import IBugTarget
300@@ -857,7 +857,8 @@
301 Select(SpecificationSubscription.specificationID,
302 [SpecificationSubscription.person == self]
303 )))
304- clauses = [Or(*role_clauses), get_specification_privacy_filter(user)]
305+ tables, clauses = visible_specification_query(user)
306+ clauses.append(Or(*role_clauses))
307 # Defaults for completeness: if nothing is said about completeness
308 # then we want to show INCOMPLETE.
309 if SpecificationFilter.COMPLETE not in filter:
310@@ -867,7 +868,7 @@
311 filter.add(SpecificationFilter.INCOMPLETE)
312
313 clauses.extend(get_specification_filters(filter))
314- results = Store.of(self).find(Specification, *clauses)
315+ results = Store.of(self).using(*tables).find(Specification, *clauses)
316 # The default sort is priority descending, so only explictly sort for
317 # DATE.
318 if sort == SpecificationSort.DATE:
319@@ -876,6 +877,7 @@
320 sort = None
321 if sort is not None:
322 results = results.order_by(sort)
323+ results.config(distinct=True)
324 if quantity is not None:
325 results = results[:quantity]
326 return results
327@@ -1466,23 +1468,23 @@
328 from lp.registry.model.distribution import Distribution
329 store = Store.of(self)
330 WorkItem = SpecificationWorkItem
331- origin = [
332- WorkItem,
333- Join(Specification, WorkItem.specification == Specification.id),
334+ origin, query = visible_specification_query(user)
335+ origin.extend([
336+ Join(WorkItem, WorkItem.specification == Specification.id),
337 # WorkItems may not have a milestone and in that case they inherit
338 # the one from the spec.
339 Join(Milestone,
340 Coalesce(WorkItem.milestone_id,
341 Specification.milestoneID) == Milestone.id),
342- ]
343+ ])
344 today = datetime.today().date()
345- query = And(
346- get_specification_privacy_filter(user),
347+ query.extend([
348 Milestone.dateexpected <= date, Milestone.dateexpected >= today,
349 WorkItem.deleted == False,
350 OR(WorkItem.assignee_id.is_in(self.participant_ids),
351- Specification.assigneeID.is_in(self.participant_ids)))
352- result = store.using(*origin).find(WorkItem, query)
353+ Specification.assigneeID.is_in(self.participant_ids))])
354+ result = store.using(*origin).find(WorkItem, *query)
355+ result.config(distinct=True)
356
357 def eager_load(workitems):
358 specs = bulk.load_related(
359@@ -2844,6 +2846,8 @@
360 pass
361 elif uploader_only:
362 lpspr = ClassAlias(LatestPersonSourcePackageReleaseCache, 'lpspr')
363+ upload_distroseries_id = (
364+ LatestPersonSourcePackageReleaseCache.upload_distroseries_id)
365 clauses.append(Not(Exists(Select(1,
366 where=And(
367 lpspr.sourcepackagename_id ==
368@@ -2851,7 +2855,7 @@
369 lpspr.upload_archive_id ==
370 LatestPersonSourcePackageReleaseCache.upload_archive_id,
371 lpspr.upload_distroseries_id ==
372- LatestPersonSourcePackageReleaseCache.upload_distroseries_id,
373+ upload_distroseries_id,
374 lpspr.archive_purpose != ArchivePurpose.PPA,
375 lpspr.maintainer_id == self.id),
376 tables=lpspr))))
377
378=== modified file 'lib/lp/registry/model/product.py'
379--- lib/lp/registry/model/product.py 2012-11-15 23:05:42 +0000
380+++ lib/lp/registry/model/product.py 2012-11-16 20:58:25 +0000
381@@ -97,11 +97,11 @@
382 )
383 from lp.blueprints.model.specification import (
384 get_specification_filters,
385- get_specification_privacy_filter,
386 HasSpecificationsMixin,
387 Specification,
388 SPECIFICATION_POLICY_ALLOWED_TYPES,
389 SPECIFICATION_POLICY_DEFAULT_TYPES,
390+ visible_specification_query,
391 )
392 from lp.blueprints.model.sprint import HasSprintsMixin
393 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
394@@ -1408,14 +1408,15 @@
395 # - completeness.
396 # - informational.
397 #
398- clauses = [Specification.product == self,
399- get_specification_privacy_filter(user)]
400+ tables, clauses = visible_specification_query(user)
401+ clauses.append(Specification.product == self)
402 clauses.extend(get_specification_filters(filter))
403 if prejoin_people:
404- results = self._preload_specifications_people(clauses)
405+ results = self._preload_specifications_people(tables, clauses)
406 else:
407- results = Store.of(self).find(Specification, *clauses)
408- results.order_by(order)
409+ tableset = Store.of(self).using(*tables)
410+ results = tableset.find(Specification, *clauses)
411+ results.order_by(order).config(distinct=True)
412 if quantity is not None:
413 results = results[:quantity]
414 return results
415
416=== modified file 'lib/lp/registry/model/sharingjob.py'
417--- lib/lp/registry/model/sharingjob.py 2012-09-28 06:15:58 +0000
418+++ lib/lp/registry/model/sharingjob.py 2012-11-16 20:58:25 +0000
419@@ -44,8 +44,8 @@
420 from lp.app.enums import InformationType
421 from lp.blueprints.interfaces.specification import ISpecification
422 from lp.blueprints.model.specification import (
423- get_specification_privacy_filter,
424 Specification,
425+ visible_specification_query,
426 )
427 from lp.blueprints.model.specificationsubscription import (
428 SpecificationSubscription,
429@@ -440,8 +440,7 @@
430 sub.person, self.requestor, ignore_permissions=True)
431 if specification_filters:
432 specification_filters.append(
433- Not(get_specification_privacy_filter(
434- SpecificationSubscription.personID)))
435+ spec_not_visible(SpecificationSubscription.personID))
436 tables = (
437 SpecificationSubscription,
438 Join(
439@@ -455,3 +454,10 @@
440 for sub in specifications_subscriptions:
441 sub.specification.unsubscribe(
442 sub.person, self.requestor, ignore_permissions=True)
443+
444+
445+def spec_not_visible(person_id):
446+ """Return an expression for finding specs not visible to the person."""
447+ tables, clauses = visible_specification_query(person_id)
448+ subselect = Select(Specification.id, tables=tables, where=And(clauses))
449+ return Not(Specification.id.is_in(subselect))