Merge lp:~abentley/launchpad/optimize-spec-query into lp:launchpad

Proposed by Aaron Bentley on 2012-11-09
Status: Merged
Approved by: Aaron Bentley on 2012-11-09
Approved revision: no longer in the source branch.
Merged at revision: 16258
Proposed branch: lp:~abentley/launchpad/optimize-spec-query
Merge into: lp:launchpad
Diff against target: 172 lines (+72/-16)
2 files modified
lib/lp/blueprints/model/specification.py (+51/-4)
lib/lp/blueprints/model/sprint.py (+21/-12)
To merge this branch: bzr merge lp:~abentley/launchpad/optimize-spec-query
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code 2012-11-09 Approve on 2012-11-09
Review via email: mp+133716@code.launchpad.net

Commit Message

Optimise specification-for-sprint queries.

Description of the Change

= Summary =
Fix bug #1075365: Timeout when trying to visit previous sprint pages

== Proposed fix ==
Optimize specification sprint queries.

== Pre-implementation notes ==
Actual query profiling done with deryck.

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
Flatten all the privacy filter code using LEFT JOINs, so that the query planner has more freedom to choose the correct plan.

This code introduces visible_specification_query, but does not remove get_specification_privacy_filter, because we want to first be sure that visible_specification_query is performant on production. We have measured its performance at 276 ms on staging and 543ms on production.

Because visible_specification_query uses left joins, it and spec_filter_clause return both a list of tables and a list of clauses.

DISTINCT is required because a user may have multiple AccessPolicyGrantFlat entries for a given artifact. In what is arguably buggy behaviour, Storm does not specify ON for SELECT DISTINCT when a custom ordering is specified and the result set is configured with distinct=True. Therefore, Sprint.specifications specifies the particular distinct clause to use.

== Tests ==
bin/test test_sprint

== Demo and Q/A ==
https://staging.launchpad.net/sprints/uds-r should not time out. (Cannot use wiht qastaging, because it has stale data).

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/model/sprint.py

To post a comment you must log in.
Brad Crittenden (bac) wrote :

This approach looks reasonable to me Aaron. Please remember to follow up to remove the obsoleted code after QA.

review: Approve (code)
Aaron Bentley (abentley) wrote :

As discussed on IRC, the "obsolete" code currently has other call sites. I do plan to remove it, but that will involve more changes.

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-10-30 05:07:30 +0000
3+++ lib/lp/blueprints/model/specification.py 2012-11-12 16:53:21 +0000
4@@ -13,6 +13,7 @@
5 'SPECIFICATION_POLICY_DEFAULT_TYPES',
6 'SpecificationSet',
7 'spec_started_clause',
8+ 'visible_specification_query',
9 ]
10
11 from lazr.lifecycle.event import (
12@@ -1281,17 +1282,63 @@
13 AccessArtifact.specification_id == Specification.id))))
14
15
16-def get_specification_filters(filter):
17+def visible_specification_query(user):
18+ """Return a Storm expression and list of tables for filtering
19+ specifications by privacy.
20+
21+ :param user: A Person ID or a column reference.
22+ :return: A tuple of tables, clauses to filter out specifications that the
23+ user cannot see.
24+ """
25+ from lp.registry.model.product import Product
26+ from lp.registry.model.accesspolicy import (
27+ AccessArtifact,
28+ AccessPolicy,
29+ AccessPolicyGrantFlat,
30+ )
31+ tables = [
32+ Specification,
33+ LeftJoin(Product, Specification.productID == Product.id),
34+ LeftJoin(AccessPolicy, And(
35+ Or(Specification.productID == AccessPolicy.product_id,
36+ Specification.distributionID ==
37+ AccessPolicy.distribution_id),
38+ Specification.information_type == AccessPolicy.type)),
39+ LeftJoin(AccessPolicyGrantFlat,
40+ AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
41+ LeftJoin(
42+ TeamParticipation,
43+ And(AccessPolicyGrantFlat.grantee == TeamParticipation.teamID,
44+ TeamParticipation.person == user)),
45+ LeftJoin(AccessArtifact,
46+ AccessPolicyGrantFlat.abstract_artifact_id ==
47+ AccessArtifact.id)
48+ ]
49+ clauses = [
50+ Or(Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES),
51+ And(AccessPolicyGrantFlat.id != None,
52+ TeamParticipation.personID != None,
53+ Or(AccessPolicyGrantFlat.abstract_artifact == None,
54+ AccessArtifact.specification_id == Specification.id))),
55+ Or(Specification.product == None, Product.active == True)]
56+ return tables, clauses
57+
58+
59+def get_specification_filters(filter, assume_product_active=False):
60 """Return a list of Storm expressions for filtering Specifications.
61
62 :param filters: A collection of SpecificationFilter and/or strings.
63 Strings are used for text searches.
64+ :param assume_product_active: If True, assume the Product is active,
65+ instead of ensuring it is active.
66 """
67 from lp.registry.model.product import Product
68+ clauses = []
69 # If Product is used, it must be active.
70- clauses = [Or(Specification.product == None,
71- Not(Specification.productID.is_in(Select(Product.id,
72- Product.active == False))))]
73+ if not assume_product_active:
74+ clauses.extend([Or(Specification.product == None,
75+ Not(Specification.productID.is_in(Select(Product.id,
76+ Product.active == False))))])
77 # ALL is the trump card.
78 if SpecificationFilter.ALL in filter:
79 return clauses
80
81=== modified file 'lib/lp/blueprints/model/sprint.py'
82--- lib/lp/blueprints/model/sprint.py 2012-10-15 23:35:20 +0000
83+++ lib/lp/blueprints/model/sprint.py 2012-11-12 16:53:21 +0000
84@@ -17,6 +17,7 @@
85 )
86 from storm.locals import (
87 Desc,
88+ Join,
89 Or,
90 Store,
91 )
92@@ -40,8 +41,8 @@
93 )
94 from lp.blueprints.model.specification import (
95 get_specification_filters,
96- get_specification_privacy_filter,
97 HasSpecificationsMixin,
98+ visible_specification_query,
99 )
100 from lp.blueprints.model.sprintattendance import SprintAttendance
101 from lp.blueprints.model.sprintspecification import SprintSpecification
102@@ -121,9 +122,12 @@
103 """
104 # import here to avoid circular deps
105 from lp.blueprints.model.specification import Specification
106- query = [SprintSpecification.sprintID == self.id,
107- SprintSpecification.specificationID == Specification.id]
108- query.append(get_specification_privacy_filter(user))
109+ tables, query = visible_specification_query(user)
110+ tables.append(Join(
111+ SprintSpecification,
112+ SprintSpecification.specification == Specification.id
113+ ))
114+ query.extend([SprintSpecification.sprintID == self.id])
115 if not filter:
116 # filter could be None or [] then we decide the default
117 # which for a sprint is to show everything approved
118@@ -151,8 +155,9 @@
119 if len(statuses) > 0:
120 query.append(Or(*statuses))
121 # Filter for specification text
122- query.extend(get_specification_filters(filter))
123- return query
124+ query.extend(
125+ get_specification_filters(filter, assume_product_active=True))
126+ return tables, query
127
128 def all_specifications(self, user):
129 return self.specifications(user, filter=[SpecificationFilter.ALL])
130@@ -165,12 +170,14 @@
131 assert not prejoin_people
132 if filter is None:
133 filter = set([SpecificationFilter.ACCEPTED])
134- query = self.spec_filter_clause(user, filter=filter)
135+ tables, query = self.spec_filter_clause(user, filter)
136 # import here to avoid circular deps
137 from lp.blueprints.model.specification import Specification
138- results = Store.of(self).find(Specification, *query)
139+ store = Store.of(self)
140+ results = store.using(*tables).find(Specification, *query)
141 if sort == SpecificationSort.DATE:
142 order = (Desc(SprintSpecification.date_created), Specification.id)
143+ distinct = [SprintSpecification.date_created, Specification.id]
144 # we need to establish if the listing will show specs that have
145 # been decided only, or will include proposed specs.
146 if (SpecificationFilter.ALL not in filter and
147@@ -178,19 +185,21 @@
148 # this will show only decided specs so use the date the spec
149 # was accepted or declined for the sprint
150 order = (Desc(SprintSpecification.date_decided),) + order
151+ distinct = [SprintSpecification.date_decided] + distinct
152 results = results.order_by(*order)
153 else:
154 assert sort is None or sort == SpecificationSort.PRIORITY
155 # fall back to default, which is priority, descending.
156+ distinct = True
157 if quantity is not None:
158 results = results[:quantity]
159- return results
160+ return results.config(distinct=distinct)
161
162 def specificationLinks(self, filter=None):
163 """See `ISprint`."""
164- query = self.spec_filter_clause(None, filter=filter)
165- result = Store.of(self).find(SprintSpecification, *query)
166- return result
167+ tables, query = self.spec_filter_clause(None, filter=filter)
168+ t_set = Store.of(self).using(*tables)
169+ return t_set.find(SprintSpecification, *query).config(distinct=True)
170
171 def getSpecificationLink(self, speclink_id):
172 """See `ISprint`.