Merge lp:~wgrant/launchpad/sharing-prettier-sql into lp:launchpad

Proposed by William Grant on 2012-03-23
Status: Merged
Approved by: William Grant on 2012-03-23
Approved revision: no longer in the source branch.
Merged at revision: 15002
Proposed branch: lp:~wgrant/launchpad/sharing-prettier-sql
Merge into: lp:launchpad
Diff against target: 273 lines (+82/-67)
4 files modified
lib/lp/registry/model/accesspolicy.py (+34/-33)
lib/lp/registry/services/sharingservice.py (+10/-15)
lib/lp/registry/services/tests/test_sharingservice.py (+15/-13)
lib/lp/registry/tests/test_accesspolicy.py (+23/-6)
To merge this branch: bzr merge lp:~wgrant/launchpad/sharing-prettier-sql
Reviewer Review Type Date Requested Status
Ian Booth (community) 2012-03-23 Approve on 2012-03-23
Review via email: mp+98967@code.launchpad.net

Commit Message

Rework findGranteePermissionsByPolicy to be fast and to batch correctly.

Description of the Change

This branch reworks findGranteePermissionsByPolicy to be fast and to batch correctly.

Currently the primary query sorts the people then eliminates duplicates. But people often have tens or even thousands of rows, so we save a lot of time by doing uniqueness first. I moved the distinct into a subquery to ensure this, and for Ubuntu it's roughly 4x faster on DF than the old query on qastaging.

Batches derived from the method's output usually have less than the desired number of rows. This is because the UI slices by person, but the method slices by (person, policy), so people with more than one policy will cause fewer people to be returned. I reworked findGranteePermissionsByPolicy and jsonShareeData to handle one result row per person containing a dict of {policy: permission, [...]}, instead of multiple rows each containing a single (policy, permission).

To post a comment you must log in.
Ian Booth (wallyworld) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/accesspolicy.py'
2--- lib/lp/registry/model/accesspolicy.py 2012-03-22 13:01:27 +0000
3+++ lib/lp/registry/model/accesspolicy.py 2012-03-23 04:21:26 +0000
4@@ -11,10 +11,14 @@
5 'AccessPolicyGrant',
6 ]
7
8+from collections import defaultdict
9+
10 import pytz
11 from storm.expr import (
12 And,
13+ In,
14 Or,
15+ Select,
16 SQL,
17 )
18 from storm.properties import (
19@@ -353,51 +357,48 @@
20 @classmethod
21 def findGranteePermissionsByPolicy(cls, policies, grantees=None):
22 """See `IAccessPolicyGrantFlatSource`."""
23- ids = [policy.id for policy in policies]
24-
25- # A cache for the sharing permissions, keyed on (grantee.id, policy.id)
26- permissions_cache = {}
27-
28- def set_permission(row):
29- # row contains (grantee.id, policy.id, permission_placeholder)
30- # Lookup the permission from the previously loaded cache.
31- return (
32- row[0], row[1], permissions_cache.get((row[0].id, row[1].id)))
33-
34- def load_permissions(rows):
35+ policies_by_id = dict((policy.id, policy) for policy in policies)
36+
37+ # A cache for the sharing permissions, keyed on grantee
38+ permissions_cache = defaultdict(dict)
39+
40+ def set_permission(person):
41+ # Lookup the permissions from the previously loaded cache.
42+ return (person[0], permissions_cache[person[0]])
43+
44+ def load_permissions(people):
45 # We now have the grantees and policies we want in the result so
46 # load any corresponding permissions and cache them.
47- person_ids = set(row[0].id for row in rows)
48- policy_ids = set(row[1].id for row in rows)
49- sharing_permission_term = SQL("""
50- CASE(
51- MIN(COALESCE(artifact, 0)))
52- WHEN 0 THEN '%s'
53- ELSE '%s'
54- END
55- """% (SharingPermission.ALL.name, SharingPermission.SOME.name))
56+ people_by_id = dict(
57+ (person[0].id, person[0]) for person in people)
58+ sharing_permission_term = SQL(
59+ "CASE MIN(COALESCE(artifact, 0)) WHEN 0 THEN ? ELSE ? END",
60+ (SharingPermission.ALL.name, SharingPermission.SOME.name))
61 constraints = [
62- cls.grantee_id.is_in(person_ids),
63- cls.policy_id.is_in(policy_ids)]
64+ cls.grantee_id.is_in(people_by_id.keys()),
65+ cls.policy_id.is_in(policies_by_id.keys())]
66 result_set = IStore(cls).find(
67 (cls.grantee_id, cls.policy_id, sharing_permission_term),
68 *constraints).group_by(cls.grantee_id, cls.policy_id)
69 for (person_id, policy_id, permission) in result_set:
70- permissions_cache[(person_id, policy_id)] = (
71- SharingPermission.items[permission])
72+ person = people_by_id[person_id]
73+ policy = policies_by_id[policy_id]
74+ permissions_cache[person][policy] = (
75+ SharingPermission.items[str(permission)])
76
77- # The main result set has a placeholder for permission.
78- constraints = [
79- Person.id == cls.grantee_id,
80- AccessPolicy.id == cls.policy_id,
81- cls.policy_id.is_in(ids)]
82+ constraints = [cls.policy_id.is_in(policies_by_id.keys())]
83 if grantees:
84 grantee_ids = [grantee.id for grantee in grantees]
85 constraints.append(cls.grantee_id.is_in(grantee_ids))
86+ # Since the sort time dominates this query, we do the DISTINCT
87+ # in a subquery to ensure it's performed first.
88 result_set = IStore(cls).find(
89- (Person, AccessPolicy,
90- SQL("'%s' as permission" % SharingPermission.NOTHING.name)),
91- *constraints).config(distinct=True)
92+ (Person,),
93+ In(
94+ Person.id,
95+ Select(
96+ (cls.grantee_id,), where=And(*constraints),
97+ distinct=True)))
98 return DecoratedResultSet(
99 result_set,
100 result_decorator=set_permission, pre_iter_hook=load_permissions)
101
102=== modified file 'lib/lp/registry/services/sharingservice.py'
103--- lib/lp/registry/services/sharingservice.py 2012-03-22 09:00:36 +0000
104+++ lib/lp/registry/services/sharingservice.py 2012-03-23 04:21:26 +0000
105@@ -117,23 +117,18 @@
106 def jsonShareeData(self, grant_permissions):
107 """See `ISharingService`."""
108 result = []
109- person_by_id = {}
110 request = get_current_web_service_request()
111 browser_request = IWebBrowserOriginatingRequest(request)
112- for (grantee, policy, sharing_permission) in grant_permissions:
113- if not grantee.id in person_by_id:
114- person_data = {
115- 'name': grantee.name,
116- 'meta': 'team' if grantee.is_team else 'person',
117- 'display_name': grantee.displayname,
118- 'self_link': absoluteURL(grantee, request),
119- 'permissions': {}}
120- person_data['web_link'] = absoluteURL(grantee, browser_request)
121- person_by_id[grantee.id] = person_data
122- result.append(person_data)
123- person_data = person_by_id[grantee.id]
124- person_data['permissions'][policy.type.name] = (
125- sharing_permission.name)
126+ for (grantee, permissions) in grant_permissions:
127+ result.append({
128+ 'name': grantee.name,
129+ 'meta': 'team' if grantee.is_team else 'person',
130+ 'display_name': grantee.displayname,
131+ 'self_link': absoluteURL(grantee, request),
132+ 'web_link': absoluteURL(grantee, browser_request),
133+ 'permissions': dict(
134+ (policy.type.name, permission.name)
135+ for (policy, permission) in permissions.iteritems())})
136 return result
137
138 @available_with_permission('launchpad.Edit', 'pillar')
139
140=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
141--- lib/lp/registry/services/tests/test_sharingservice.py 2012-03-22 12:11:34 +0000
142+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-03-23 04:21:26 +0000
143@@ -125,15 +125,18 @@
144
145 def test_jsonShareeData(self):
146 # jsonShareeData returns the expected data.
147- access_policy = self.factory.makeAccessPolicy()
148+ product = self.factory.makeProduct()
149+ [policy1, policy2] = getUtility(IAccessPolicySource).findByPillar(
150+ [product])
151 grantee = self.factory.makePerson()
152 sharees = self.service.jsonShareeData(
153- [(grantee, access_policy, SharingPermission.ALL),
154- (grantee, access_policy, SharingPermission.SOME)])
155+ [(grantee, {
156+ policy1: SharingPermission.ALL,
157+ policy2: SharingPermission.SOME})])
158 expected_data = self._makeShareeData(
159 grantee,
160- [(access_policy.type, SharingPermission.ALL),
161- (access_policy.type, SharingPermission.SOME)])
162+ [(policy1.type, SharingPermission.ALL),
163+ (policy2.type, SharingPermission.SOME)])
164 self.assertContentEqual([expected_data], sharees)
165
166 def _assert_getPillarShareeData(self, pillar):
167@@ -256,8 +259,8 @@
168
169 sharees = self.service.getPillarSharees(pillar)
170 expected_sharees = [
171- (grantee, access_policy, SharingPermission.ALL),
172- (artifact_grant.grantee, access_policy, SharingPermission.SOME)]
173+ (grantee, {access_policy: SharingPermission.ALL}),
174+ (artifact_grant.grantee, {access_policy: SharingPermission.SOME})]
175 self.assertContentEqual(expected_sharees, sharees)
176
177 def test_getProductSharees(self):
178@@ -355,10 +358,9 @@
179 self.assertEqual(expected_sharee_data, sharee_data)
180 # Check that getPillarSharees returns what we expect.
181 expected_sharee_grants = [
182- (sharee, policy, permission)
183- for policy, permission in [
184- (es_policy, SharingPermission.ALL),
185- (ud_policy, SharingPermission.SOME)]]
186+ (sharee, {
187+ es_policy: SharingPermission.ALL,
188+ ud_policy: SharingPermission.SOME})]
189 sharee_grants = list(self.service.getPillarSharees(pillar))
190 self.assertContentEqual(expected_sharee_grants, sharee_grants)
191
192@@ -470,11 +472,11 @@
193 access_policy for access_policy in access_policies
194 if access_policy.type in expected_information_types]
195 expected_data = [
196- (grantee, policy, SharingPermission.ALL)
197+ (grantee, {policy: SharingPermission.ALL})
198 for policy in expected_policies]
199 # Add the expected data for the other sharee.
200 another_person_data = (
201- another, access_policies[0], SharingPermission.ALL)
202+ another, {access_policies[0]: SharingPermission.ALL})
203 expected_data.append(another_person_data)
204 self.assertContentEqual(
205 expected_data, self.service.getPillarSharees(pillar))
206
207=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
208--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-22 09:00:36 +0000
209+++ lib/lp/registry/tests/test_accesspolicy.py 2012-03-23 04:21:26 +0000
210@@ -24,6 +24,7 @@
211 IAccessPolicyGrantSource,
212 IAccessPolicySource,
213 )
214+from lp.registry.model.person import Person
215 from lp.services.database.lpstorm import IStore
216 from lp.testing import TestCaseWithFactory
217 from lp.testing.layers import DatabaseFunctionalLayer
218@@ -480,14 +481,18 @@
219 policy = self.factory.makeAccessPolicy()
220 policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
221 self.assertContentEqual(
222- [(policy_grant.grantee, policy, SharingPermission.ALL)],
223+ [(policy_grant.grantee, {policy: SharingPermission.ALL})],
224 apgfs.findGranteePermissionsByPolicy(
225 [policy, policy_with_no_grantees]))
226
227 # But not people with grants on artifacts.
228- artifact_grant = self.factory.makeAccessArtifactGrant()
229+ artifact = self.factory.makeAccessArtifact()
230+ artifact_grant = self.factory.makeAccessArtifactGrant(
231+ artifact=artifact, grantee=policy_grant.grantee)
232+ other_artifact_grant = self.factory.makeAccessArtifactGrant(
233+ artifact=artifact)
234 self.assertContentEqual(
235- [(policy_grant.grantee, policy, SharingPermission.ALL)],
236+ [(policy_grant.grantee, {policy: SharingPermission.ALL})],
237 apgfs.findGranteePermissionsByPolicy(
238 [policy, policy_with_no_grantees]))
239
240@@ -496,11 +501,23 @@
241 self.factory.makeAccessPolicyArtifact(
242 artifact=artifact_grant.abstract_artifact, policy=another_policy)
243 self.assertContentEqual(
244- [(policy_grant.grantee, policy, SharingPermission.ALL),
245- (artifact_grant.grantee, another_policy, SharingPermission.SOME)],
246+ [(policy_grant.grantee, {
247+ policy: SharingPermission.ALL,
248+ another_policy: SharingPermission.SOME}),
249+ (other_artifact_grant.grantee, {
250+ another_policy: SharingPermission.SOME})],
251 apgfs.findGranteePermissionsByPolicy([
252 policy, another_policy, policy_with_no_grantees]))
253
254+ # Slicing works by person, not by (person, policy).
255+ self.assertContentEqual(
256+ [(policy_grant.grantee, {
257+ policy: SharingPermission.ALL,
258+ another_policy: SharingPermission.SOME})],
259+ apgfs.findGranteePermissionsByPolicy([
260+ policy, another_policy, policy_with_no_grantees]).order_by(
261+ Person.id)[:1])
262+
263 def test_findGranteePermissionsByPolicy_filter_grantees(self):
264 # findGranteePermissionsByPolicy() returns anyone with a grant for any
265 # of the policies or the policies' artifacts so long as the grantee is
266@@ -516,7 +533,7 @@
267 self.factory.makeAccessPolicyGrant(
268 policy=policy, grantee=grantee_not_in_result)
269 self.assertContentEqual(
270- [(policy_grant.grantee, policy, SharingPermission.ALL)],
271+ [(policy_grant.grantee, {policy: SharingPermission.ALL})],
272 apgfs.findGranteePermissionsByPolicy(
273 [policy], [grantee_in_result]))
274