Merge lp:~canonical-isd-hackers/canonical-identity-provider/sql-limit-for-openid-transactions into lp:canonical-identity-provider/release

Proposed by Łukasz Czyżykowski
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: no longer in the source branch.
Merged at revision: 225
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/sql-limit-for-openid-transactions
Merge into: lp:canonical-identity-provider/release
Diff against target: 446 lines (+120/-71)
10 files modified
identityprovider/auth.py (+1/-1)
identityprovider/forms.py (+8/-11)
identityprovider/models/account.py (+21/-15)
identityprovider/models/openidmodels.py (+33/-11)
identityprovider/models/team.py (+15/-15)
identityprovider/tests/test_models_openidmodels.py (+5/-9)
identityprovider/tests/test_views_server.py (+16/-1)
identityprovider/tests/test_views_ui.py (+4/-0)
identityprovider/views/server.py (+1/-1)
identityprovider/views/utils.py (+16/-7)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/sql-limit-for-openid-transactions
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+74234@code.launchpad.net

Commit message

Database access improvements during OpenID transaction.

Description of the change

Overview
========
This branch limits number of SQL queries executed while processing OpenID transaction.

Improvement
===========
During an OpenID request, with all possible SReg fields and Teams extension, before this branch, there are 28 SQL queries executed. With this branch, that number is down to 12. The raw speed improvement is hard to quantify, as it requires database configured in very similar way as the production one.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'identityprovider/auth.py'
2--- identityprovider/auth.py 2011-07-27 13:00:22 +0000
3+++ identityprovider/auth.py 2011-09-06 14:43:24 +0000
4@@ -11,7 +11,7 @@
5
6 from identityprovider.models import Account, AccountPassword
7 from identityprovider.models.api import APIUser
8-from identityprovider.models.const import EmailStatus, AccountStatus
9+from identityprovider.models.const import EmailStatus
10 from identityprovider.utils import (
11 validate_launchpad_password, get_object_or_none)
12
13
14=== modified file 'identityprovider/forms.py'
15--- identityprovider/forms.py 2011-08-25 13:26:06 +0000
16+++ identityprovider/forms.py 2011-09-06 14:43:24 +0000
17@@ -418,7 +418,7 @@
18 """Get the list of teams actually approved by the user for the request.
19 """
20 if self.request_method == 'POST':
21- return [t for t in self.data if t in self.request.POST]
22+ return [t for t in self.memberships if t in self.request.POST]
23 else:
24 return []
25
26@@ -428,16 +428,13 @@
27 self.teams_request = teams_request
28 self.rpconfig = rpconfig
29 self.approved_data = approved_data
30- self.data = self._get_teams_for_user(request.user,
31- rpconfig and rpconfig.can_query_any_team)
32-
33- super(TeamsRequestForm, self).__init__(self.data)
34- self._init_fields(self.data)
35- all_teams = self.teams_request.allRequestedTeams()
36- membership = TeamMembership.get_team_memberships_for_user(
37- all_teams, self.request.user,
38- self.rpconfig and self.rpconfig.can_query_any_team)
39- self.should_display = len(membership) > 0
40+ self.memberships = self._get_teams_for_user(
41+ request.user, rpconfig and rpconfig.can_query_any_team)
42+
43+ super(TeamsRequestForm, self).__init__(self.memberships)
44+
45+ self._init_fields(self.memberships)
46+ self.should_display = len(self.memberships) > 0
47
48 def _get_teams_for_user(self, user, include_private=False):
49 """Get the list of teams to ask about in the form based on the user's
50
51=== modified file 'identityprovider/models/account.py'
52--- identityprovider/models/account.py 2011-07-27 13:00:22 +0000
53+++ identityprovider/models/account.py 2011-09-06 14:43:24 +0000
54@@ -130,20 +130,23 @@
55 return sites.order_by('-date_last_used')
56
57 def _get_preferredemail(self):
58- try:
59- return self.emailaddress_set.filter(
60- status=EmailStatus.PREFERRED)[0]
61- except IndexError:
62- # Try to determine a suitable address, and mark it as preferred
63+ if not hasattr(self, '_preferredemail'):
64 try:
65- email = self.emailaddress_set.filter(
66- status=EmailStatus.VALIDATED).order_by('date_created')[0]
67- email.status = EmailStatus.PREFERRED
68- email.save()
69- return email
70+ self._preferredemail = self.emailaddress_set.filter(
71+ status=EmailStatus.PREFERRED)[0]
72 except IndexError:
73- # Now we really don't have anything
74- return None
75+ # Try to determine a suitable address, and mark it as preferred
76+ try:
77+ emails = self.emailaddress_set.filter(
78+ status=EmailStatus.VALIDATED)
79+ email = emails.order_by('date_created')[0]
80+ email.status = EmailStatus.PREFERRED
81+ email.save()
82+ self._preferredemail = email
83+ except IndexError:
84+ # Now we really don't have anything
85+ self._preferredemail = None
86+ return self._preferredemail
87
88 def _set_preferredemail(self, email):
89 current = self.preferredemail
90@@ -152,6 +155,7 @@
91 current.save()
92 email.status = EmailStatus.PREFERRED
93 email.save()
94+ self._preferredemail = email
95
96 preferredemail = property(_get_preferredemail, _set_preferredemail)
97
98@@ -166,9 +170,11 @@
99 lp_account = open_ids[0].lp_account
100 # Importing it here to prevent cyclic import issue
101 from .person import Person
102- persons = Person.objects.filter(lp_account=lp_account)
103- if len(persons) > 0:
104- self._person = persons[0]
105+ try:
106+ self._person = Person.objects.select_related(
107+ 'personlocation').get(lp_account=lp_account)
108+ except Person.DoesNotExist:
109+ pass
110 return getattr(self, '_person', None)
111
112 @property
113
114=== modified file 'identityprovider/models/openidmodels.py'
115--- identityprovider/models/openidmodels.py 2011-07-27 13:00:22 +0000
116+++ identityprovider/models/openidmodels.py 2011-09-06 14:43:24 +0000
117@@ -2,14 +2,19 @@
118 # GNU Affero General Public License version 3 (see the file LICENSE).
119
120 import base64
121+import hashlib
122 import time
123+
124 from datetime import datetime
125 from openid.association import Association
126+
127 import openid.store
128 import openid.store.nonce
129+
130 from openid.store.interface import OpenIDStore
131
132 from django.conf import settings
133+from django.core.cache import cache
134 from django.db import models
135 from django.utils import simplejson as json
136 from django.utils.translation import ugettext_lazy as _
137@@ -167,6 +172,16 @@
138 def __unicode__(self):
139 return self.displayname
140
141+ @classmethod
142+ def cache_key(cls, trust_root):
143+ # Need to calculate digest of the url to make it work with long trust_root
144+ # values when caching in memcached
145+ return 'rpconfig-' + hashlib.md5(trust_root.rstrip('/')).hexdigest()
146+
147+ def save(self, *args, **kwargs):
148+ cache.delete(self.cache_key(self.trust_root))
149+ return super(OpenIDRPConfig, self).save(*args, **kwargs)
150+
151 @property
152 def logo_url(self):
153 if self.logo:
154@@ -186,19 +201,30 @@
155 return None
156 if openid_identifier is None:
157 openid_identifier = account.openid_identity_url
158+ if approved_data:
159+ approved_data = json.dumps(approved_data)
160 try:
161 summary = OpenIDRPSummary.objects.get(
162 account=account,
163 trust_root=trust_root,
164 openid_identifier=openid_identifier)
165- summary.increment()
166+ update_bits = dict(
167+ total_logins=models.F('total_logins') + 1,
168+ date_last_used=datetime.utcnow(),
169+ )
170+ if approved_data:
171+ update_bits['approved_data'] = approved_data
172+ # Using update is not causing Django to select the record once
173+ # again from the database, it's one less SELECT.
174+ OpenIDRPSummary.objects.filter(pk=summary.pk).update(**update_bits)
175 except OpenIDRPSummary.DoesNotExist:
176- summary = OpenIDRPSummary.objects.create(
177+ summary = OpenIDRPSummary(
178 account=account,
179 trust_root=trust_root,
180 openid_identifier=openid_identifier)
181- if approved_data:
182- summary.set_approved_data(approved_data)
183+ if approved_data:
184+ summary.approved_data = approved_data
185+ summary.save()
186 return summary
187
188
189@@ -220,11 +246,6 @@
190 db_table = u'openidrpsummary'
191 unique_together = ('account', 'trust_root', 'openid_identifier')
192
193- def increment(self):
194- self.total_logins += 1
195- self.date_last_used = datetime.utcnow()
196- self.save()
197-
198 def get_approved_data(self):
199 try:
200 data = json.loads(self.approved_data)
201@@ -233,8 +254,9 @@
202 return data
203
204 def set_approved_data(self, approved_data):
205- self.approved_data = json.dumps(approved_data)
206- self.save()
207+ if not isinstance(approved_data, basestring):
208+ approved_data = json.dumps(approved_data)
209+ self.approved_data = approved_data
210
211
212 class DjangoOpenIDStore(OpenIDStore):
213
214=== modified file 'identityprovider/models/team.py'
215--- identityprovider/models/team.py 2011-07-01 19:47:04 +0000
216+++ identityprovider/models/team.py 2011-09-06 14:43:24 +0000
217@@ -50,21 +50,21 @@
218
219 @staticmethod
220 def get_team_memberships_for_user(team_names, user, include_private=False):
221- memberships = []
222- for team_name in team_names:
223- try:
224- team = Person.objects.get(name=team_name)
225- except Person.DoesNotExist:
226- team = None
227- if team is None or not team.is_team():
228- continue
229- # Control access to private teams
230- if (team.visibility != PERSON_VISIBILITY_PUBLIC and
231- not include_private):
232- continue
233- if user.person_in_team(team):
234- memberships.append(team_name)
235- return memberships
236+ teams = Person.objects.filter(
237+ name__in=team_names, # All team names provided
238+ teamowner__isnull=False, # Only teams
239+ )
240+ if not include_private:
241+ teams = teams.filter(
242+ visibility=PERSON_VISIBILITY_PUBLIC)
243+ # Only teams in which the user is a member, which can be either through
244+ # direct membership or by being a team owner.
245+ teams = teams.filter(
246+ models.Q(team_participations__person=user.person) |
247+ models.Q(teamowner__id=models.F('id')),
248+ )
249+ # By default it's a query set result, not a true list
250+ return list(teams.values_list('name', flat=True))
251
252
253 class TeamParticipation(models.Model):
254
255=== modified file 'identityprovider/tests/test_models_openidmodels.py'
256--- identityprovider/tests/test_models_openidmodels.py 2011-07-27 13:00:22 +0000
257+++ identityprovider/tests/test_models_openidmodels.py 2011-09-06 14:43:24 +0000
258@@ -253,6 +253,7 @@
259 summary = OpenIDRPSummary.objects.create(account=self.account,
260 trust_root=self.trust_root, openid_identifier='oid')
261 summary.set_approved_data(self.approved_data)
262+ summary.save()
263 return summary
264
265 def test_record_when_readonly(self):
266@@ -281,6 +282,7 @@
267 summary1 = self.create_summary()
268 summary2 = OpenIDRPSummary.objects.record(self.account,
269 self.trust_root, openid_identifier='oid')
270+ summary2 = OpenIDRPSummary.objects.get(pk=summary2.pk)
271 self.assertEqual(summary2.total_logins, 2)
272 self.assertEqual(summary1, summary2)
273
274@@ -294,23 +296,17 @@
275 self.assertNotEqual(summary1, summary2)
276
277 def test_record_not_existing(self):
278- summary1 = OpenIDRPSummary.objects.record(self.account, self.trust_root,
279- openid_identifier='oid')
280+ summary1 = OpenIDRPSummary.objects.record(
281+ self.account, self.trust_root, openid_identifier='oid')
282 self.assertEqual(summary1.total_logins, 1)
283 summary2 = OpenIDRPSummary.objects.get(account=self.account,
284 trust_root=self.trust_root, openid_identifier='oid')
285 self.assertEqual(summary1, summary2)
286
287- def test_increment(self):
288- summary = self.create_summary()
289- self.assertEqual(summary.total_logins, 1)
290-
291- summary.increment()
292- self.assertEqual(summary.total_logins, 2)
293-
294 def test_approved_data(self):
295 summary = self.create_summary()
296 summary.set_approved_data(self.approved_data)
297+ summary.save()
298 summary2 = OpenIDRPSummary.objects.get(account=self.account,
299 trust_root=self.trust_root, openid_identifier='oid')
300 self.assertEqual(summary2.get_approved_data(), self.approved_data)
301
302=== modified file 'identityprovider/tests/test_views_server.py'
303--- identityprovider/tests/test_views_server.py 2011-07-27 13:00:22 +0000
304+++ identityprovider/tests/test_views_server.py 2011-09-06 14:43:24 +0000
305@@ -7,6 +7,7 @@
306
307 from django.conf import settings
308 from django.contrib.auth.models import AnonymousUser
309+from django.core.cache import cache
310 from django.http import HttpRequest
311 from django.test import TestCase
312 from mock import Mock
313@@ -67,6 +68,11 @@
314 self.META = {}
315
316
317+def delete_rpconfig_cache_entry(trust_root):
318+ cache.delete(OpenIDRPConfig.cache_key(trust_root))
319+
320+
321+
322 class HandleOpenIDErrorTestCase(SSOBaseTestCase):
323
324 # tests for the _handle_openid_error method
325@@ -418,6 +424,8 @@
326 # create multiple matching OpenIDRPSummary objects
327 account = Account.objects.get_by_email(self.login_email)
328 trust_root = 'http://localhost/'
329+ delete_rpconfig_cache_entry(trust_root)
330+
331 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,
332 openid_identifier='http://localhost/~openid1')
333 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,
334@@ -427,6 +435,7 @@
335 self.assertEqual(r.status_code, 200)
336 self.assertTemplateUsed(r, 'decide.html')
337
338+
339 def test_decide_team_membership_with_auto_authorize(self):
340 # make sure rpconfig is set to auto authorize
341 self.client.login(username='mark@example.com',
342@@ -537,10 +546,14 @@
343 password='test')
344
345 # create a trusted rpconfig
346- rpconfig = OpenIDRPConfig(trust_root='http://untrusted/',
347+ trust_root = 'http://untrusted/'
348+ rpconfig = OpenIDRPConfig(trust_root=trust_root,
349 can_query_any_team=True,
350 description="Some description",
351 )
352+
353+ delete_rpconfig_cache_entry("http://localhost/")
354+
355 rpconfig.save()
356 param_overrides = {
357 'openid.sreg.required': 'nickname,email,fullname',
358@@ -1048,6 +1061,8 @@
359 session[token] = signed.dumps(DummyORequest())
360 session.save()
361
362+ delete_rpconfig_cache_entry("http://localhost/")
363+
364 r = self.client.get('/%s/+decide' % token)
365 self.assertContains(r, '<em>not</em>')
366
367
368=== modified file 'identityprovider/tests/test_views_ui.py'
369--- identityprovider/tests/test_views_ui.py 2011-08-25 13:25:51 +0000
370+++ identityprovider/tests/test_views_ui.py 2011-09-06 14:43:24 +0000
371@@ -522,10 +522,13 @@
372 # make sure the account is deactivated and preferred email is removed
373 _preferred_email = account.preferredemail
374 account.status = AccountStatus.DEACTIVATED
375+ # Blow out the cached value
376 account.save()
377 for email_obj in account.emailaddress_set.all():
378 email_obj.status = EmailStatus.NEW
379 email_obj.save()
380+
381+ account = Account.objects.get_by_email('test@canonical.com')
382 self.assertEqual(account.preferredemail, None)
383
384 r = self.client.post('/+forgot_password',
385@@ -539,6 +542,7 @@
386 r = self.client.post(self._token_url('+resetpassword'), data)
387 self.assertRedirects(r, '/')
388
389+ account = Account.objects.get_by_email('test@canonical.com')
390 self.assertEqual(account.preferredemail.email, 'test@canonical.com')
391
392 for email_obj in account.emailaddress_set.all():
393
394=== modified file 'identityprovider/views/server.py'
395--- identityprovider/views/server.py 2011-07-27 19:20:16 +0000
396+++ identityprovider/views/server.py 2011-09-06 14:43:24 +0000
397@@ -504,7 +504,7 @@
398 team_names = team_names.split(',')
399 teams_form = TeamsRequestForm(request,
400 TeamsRequest.fromOpenIDRequest(orequest),
401- utils.get_rpconfig(orequest.trust_root))
402+ rpconfig)
403 approved_data['teams'] = {
404 'requested': team_names,
405 'approved': teams_form.teams_approved_by_user}
406
407=== modified file 'identityprovider/views/utils.py'
408--- identityprovider/views/utils.py 2011-07-13 14:30:29 +0000
409+++ identityprovider/views/utils.py 2011-09-06 14:43:24 +0000
410@@ -1,7 +1,9 @@
411 # Copyright 2010 Canonical Ltd. This software is licensed under the
412 # GNU Affero General Public License version 3 (see the file LICENSE).
413+import hashlib
414
415 from django.conf import settings
416+from django.core.cache import cache
417
418 from identityprovider.models import OpenIDRPConfig
419
420@@ -24,12 +26,19 @@
421
422
423 def get_rpconfig(trust_root):
424- alternatives = [trust_root]
425-
426- if trust_root.endswith('/'):
427- alternatives.append(trust_root[:-1])
428- else:
429- alternatives.append(trust_root + '/')
430+ cache_key = OpenIDRPConfig.cache_key(trust_root)
431+
432+ default = object()
433+ rpconfig = cache.get(cache_key, default)
434+ if rpconfig is not default:
435+ return rpconfig
436+
437+ alternatives = [
438+ trust_root,
439+ trust_root[:-1] if trust_root.endswith('/') else (trust_root + '/'),
440+ ]
441
442 rpconfig = OpenIDRPConfig.objects.filter(trust_root__in=alternatives)
443- return rpconfig and rpconfig[0] or None
444+ rpconfig = rpconfig and rpconfig[0] or None
445+ cache.set(cache_key, rpconfig, 60 * 60 * 24)
446+ return rpconfig