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
=== modified file 'identityprovider/auth.py'
--- identityprovider/auth.py 2011-07-27 13:00:22 +0000
+++ identityprovider/auth.py 2011-09-06 14:43:24 +0000
@@ -11,7 +11,7 @@
1111
12from identityprovider.models import Account, AccountPassword12from identityprovider.models import Account, AccountPassword
13from identityprovider.models.api import APIUser13from identityprovider.models.api import APIUser
14from identityprovider.models.const import EmailStatus, AccountStatus14from identityprovider.models.const import EmailStatus
15from identityprovider.utils import (15from identityprovider.utils import (
16 validate_launchpad_password, get_object_or_none)16 validate_launchpad_password, get_object_or_none)
1717
1818
=== modified file 'identityprovider/forms.py'
--- identityprovider/forms.py 2011-08-25 13:26:06 +0000
+++ identityprovider/forms.py 2011-09-06 14:43:24 +0000
@@ -418,7 +418,7 @@
418 """Get the list of teams actually approved by the user for the request.418 """Get the list of teams actually approved by the user for the request.
419 """419 """
420 if self.request_method == 'POST':420 if self.request_method == 'POST':
421 return [t for t in self.data if t in self.request.POST]421 return [t for t in self.memberships if t in self.request.POST]
422 else:422 else:
423 return []423 return []
424424
@@ -428,16 +428,13 @@
428 self.teams_request = teams_request428 self.teams_request = teams_request
429 self.rpconfig = rpconfig429 self.rpconfig = rpconfig
430 self.approved_data = approved_data430 self.approved_data = approved_data
431 self.data = self._get_teams_for_user(request.user,431 self.memberships = self._get_teams_for_user(
432 rpconfig and rpconfig.can_query_any_team)432 request.user, rpconfig and rpconfig.can_query_any_team)
433433
434 super(TeamsRequestForm, self).__init__(self.data)434 super(TeamsRequestForm, self).__init__(self.memberships)
435 self._init_fields(self.data)435
436 all_teams = self.teams_request.allRequestedTeams()436 self._init_fields(self.memberships)
437 membership = TeamMembership.get_team_memberships_for_user(437 self.should_display = len(self.memberships) > 0
438 all_teams, self.request.user,
439 self.rpconfig and self.rpconfig.can_query_any_team)
440 self.should_display = len(membership) > 0
441438
442 def _get_teams_for_user(self, user, include_private=False):439 def _get_teams_for_user(self, user, include_private=False):
443 """Get the list of teams to ask about in the form based on the user's440 """Get the list of teams to ask about in the form based on the user's
444441
=== modified file 'identityprovider/models/account.py'
--- identityprovider/models/account.py 2011-07-27 13:00:22 +0000
+++ identityprovider/models/account.py 2011-09-06 14:43:24 +0000
@@ -130,20 +130,23 @@
130 return sites.order_by('-date_last_used')130 return sites.order_by('-date_last_used')
131131
132 def _get_preferredemail(self):132 def _get_preferredemail(self):
133 try:133 if not hasattr(self, '_preferredemail'):
134 return self.emailaddress_set.filter(
135 status=EmailStatus.PREFERRED)[0]
136 except IndexError:
137 # Try to determine a suitable address, and mark it as preferred
138 try:134 try:
139 email = self.emailaddress_set.filter(135 self._preferredemail = self.emailaddress_set.filter(
140 status=EmailStatus.VALIDATED).order_by('date_created')[0]136 status=EmailStatus.PREFERRED)[0]
141 email.status = EmailStatus.PREFERRED
142 email.save()
143 return email
144 except IndexError:137 except IndexError:
145 # Now we really don't have anything138 # Try to determine a suitable address, and mark it as preferred
146 return None139 try:
140 emails = self.emailaddress_set.filter(
141 status=EmailStatus.VALIDATED)
142 email = emails.order_by('date_created')[0]
143 email.status = EmailStatus.PREFERRED
144 email.save()
145 self._preferredemail = email
146 except IndexError:
147 # Now we really don't have anything
148 self._preferredemail = None
149 return self._preferredemail
147150
148 def _set_preferredemail(self, email):151 def _set_preferredemail(self, email):
149 current = self.preferredemail152 current = self.preferredemail
@@ -152,6 +155,7 @@
152 current.save()155 current.save()
153 email.status = EmailStatus.PREFERRED156 email.status = EmailStatus.PREFERRED
154 email.save()157 email.save()
158 self._preferredemail = email
155159
156 preferredemail = property(_get_preferredemail, _set_preferredemail)160 preferredemail = property(_get_preferredemail, _set_preferredemail)
157161
@@ -166,9 +170,11 @@
166 lp_account = open_ids[0].lp_account170 lp_account = open_ids[0].lp_account
167 # Importing it here to prevent cyclic import issue171 # Importing it here to prevent cyclic import issue
168 from .person import Person172 from .person import Person
169 persons = Person.objects.filter(lp_account=lp_account)173 try:
170 if len(persons) > 0:174 self._person = Person.objects.select_related(
171 self._person = persons[0]175 'personlocation').get(lp_account=lp_account)
176 except Person.DoesNotExist:
177 pass
172 return getattr(self, '_person', None)178 return getattr(self, '_person', None)
173179
174 @property180 @property
175181
=== modified file 'identityprovider/models/openidmodels.py'
--- identityprovider/models/openidmodels.py 2011-07-27 13:00:22 +0000
+++ identityprovider/models/openidmodels.py 2011-09-06 14:43:24 +0000
@@ -2,14 +2,19 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import base644import base64
5import hashlib
5import time6import time
7
6from datetime import datetime8from datetime import datetime
7from openid.association import Association9from openid.association import Association
10
8import openid.store11import openid.store
9import openid.store.nonce12import openid.store.nonce
13
10from openid.store.interface import OpenIDStore14from openid.store.interface import OpenIDStore
1115
12from django.conf import settings16from django.conf import settings
17from django.core.cache import cache
13from django.db import models18from django.db import models
14from django.utils import simplejson as json19from django.utils import simplejson as json
15from django.utils.translation import ugettext_lazy as _20from django.utils.translation import ugettext_lazy as _
@@ -167,6 +172,16 @@
167 def __unicode__(self):172 def __unicode__(self):
168 return self.displayname173 return self.displayname
169174
175 @classmethod
176 def cache_key(cls, trust_root):
177 # Need to calculate digest of the url to make it work with long trust_root
178 # values when caching in memcached
179 return 'rpconfig-' + hashlib.md5(trust_root.rstrip('/')).hexdigest()
180
181 def save(self, *args, **kwargs):
182 cache.delete(self.cache_key(self.trust_root))
183 return super(OpenIDRPConfig, self).save(*args, **kwargs)
184
170 @property185 @property
171 def logo_url(self):186 def logo_url(self):
172 if self.logo:187 if self.logo:
@@ -186,19 +201,30 @@
186 return None201 return None
187 if openid_identifier is None:202 if openid_identifier is None:
188 openid_identifier = account.openid_identity_url203 openid_identifier = account.openid_identity_url
204 if approved_data:
205 approved_data = json.dumps(approved_data)
189 try:206 try:
190 summary = OpenIDRPSummary.objects.get(207 summary = OpenIDRPSummary.objects.get(
191 account=account,208 account=account,
192 trust_root=trust_root,209 trust_root=trust_root,
193 openid_identifier=openid_identifier)210 openid_identifier=openid_identifier)
194 summary.increment()211 update_bits = dict(
212 total_logins=models.F('total_logins') + 1,
213 date_last_used=datetime.utcnow(),
214 )
215 if approved_data:
216 update_bits['approved_data'] = approved_data
217 # Using update is not causing Django to select the record once
218 # again from the database, it's one less SELECT.
219 OpenIDRPSummary.objects.filter(pk=summary.pk).update(**update_bits)
195 except OpenIDRPSummary.DoesNotExist:220 except OpenIDRPSummary.DoesNotExist:
196 summary = OpenIDRPSummary.objects.create(221 summary = OpenIDRPSummary(
197 account=account,222 account=account,
198 trust_root=trust_root,223 trust_root=trust_root,
199 openid_identifier=openid_identifier)224 openid_identifier=openid_identifier)
200 if approved_data:225 if approved_data:
201 summary.set_approved_data(approved_data)226 summary.approved_data = approved_data
227 summary.save()
202 return summary228 return summary
203229
204230
@@ -220,11 +246,6 @@
220 db_table = u'openidrpsummary'246 db_table = u'openidrpsummary'
221 unique_together = ('account', 'trust_root', 'openid_identifier')247 unique_together = ('account', 'trust_root', 'openid_identifier')
222248
223 def increment(self):
224 self.total_logins += 1
225 self.date_last_used = datetime.utcnow()
226 self.save()
227
228 def get_approved_data(self):249 def get_approved_data(self):
229 try:250 try:
230 data = json.loads(self.approved_data)251 data = json.loads(self.approved_data)
@@ -233,8 +254,9 @@
233 return data254 return data
234255
235 def set_approved_data(self, approved_data):256 def set_approved_data(self, approved_data):
236 self.approved_data = json.dumps(approved_data)257 if not isinstance(approved_data, basestring):
237 self.save()258 approved_data = json.dumps(approved_data)
259 self.approved_data = approved_data
238260
239261
240class DjangoOpenIDStore(OpenIDStore):262class DjangoOpenIDStore(OpenIDStore):
241263
=== modified file 'identityprovider/models/team.py'
--- identityprovider/models/team.py 2011-07-01 19:47:04 +0000
+++ identityprovider/models/team.py 2011-09-06 14:43:24 +0000
@@ -50,21 +50,21 @@
5050
51 @staticmethod51 @staticmethod
52 def get_team_memberships_for_user(team_names, user, include_private=False):52 def get_team_memberships_for_user(team_names, user, include_private=False):
53 memberships = []53 teams = Person.objects.filter(
54 for team_name in team_names:54 name__in=team_names, # All team names provided
55 try:55 teamowner__isnull=False, # Only teams
56 team = Person.objects.get(name=team_name)56 )
57 except Person.DoesNotExist:57 if not include_private:
58 team = None58 teams = teams.filter(
59 if team is None or not team.is_team():59 visibility=PERSON_VISIBILITY_PUBLIC)
60 continue60 # Only teams in which the user is a member, which can be either through
61 # Control access to private teams61 # direct membership or by being a team owner.
62 if (team.visibility != PERSON_VISIBILITY_PUBLIC and62 teams = teams.filter(
63 not include_private):63 models.Q(team_participations__person=user.person) |
64 continue64 models.Q(teamowner__id=models.F('id')),
65 if user.person_in_team(team):65 )
66 memberships.append(team_name)66 # By default it's a query set result, not a true list
67 return memberships67 return list(teams.values_list('name', flat=True))
6868
6969
70class TeamParticipation(models.Model):70class TeamParticipation(models.Model):
7171
=== modified file 'identityprovider/tests/test_models_openidmodels.py'
--- identityprovider/tests/test_models_openidmodels.py 2011-07-27 13:00:22 +0000
+++ identityprovider/tests/test_models_openidmodels.py 2011-09-06 14:43:24 +0000
@@ -253,6 +253,7 @@
253 summary = OpenIDRPSummary.objects.create(account=self.account,253 summary = OpenIDRPSummary.objects.create(account=self.account,
254 trust_root=self.trust_root, openid_identifier='oid')254 trust_root=self.trust_root, openid_identifier='oid')
255 summary.set_approved_data(self.approved_data)255 summary.set_approved_data(self.approved_data)
256 summary.save()
256 return summary257 return summary
257258
258 def test_record_when_readonly(self):259 def test_record_when_readonly(self):
@@ -281,6 +282,7 @@
281 summary1 = self.create_summary()282 summary1 = self.create_summary()
282 summary2 = OpenIDRPSummary.objects.record(self.account,283 summary2 = OpenIDRPSummary.objects.record(self.account,
283 self.trust_root, openid_identifier='oid')284 self.trust_root, openid_identifier='oid')
285 summary2 = OpenIDRPSummary.objects.get(pk=summary2.pk)
284 self.assertEqual(summary2.total_logins, 2)286 self.assertEqual(summary2.total_logins, 2)
285 self.assertEqual(summary1, summary2)287 self.assertEqual(summary1, summary2)
286288
@@ -294,23 +296,17 @@
294 self.assertNotEqual(summary1, summary2)296 self.assertNotEqual(summary1, summary2)
295297
296 def test_record_not_existing(self):298 def test_record_not_existing(self):
297 summary1 = OpenIDRPSummary.objects.record(self.account, self.trust_root,299 summary1 = OpenIDRPSummary.objects.record(
298 openid_identifier='oid')300 self.account, self.trust_root, openid_identifier='oid')
299 self.assertEqual(summary1.total_logins, 1)301 self.assertEqual(summary1.total_logins, 1)
300 summary2 = OpenIDRPSummary.objects.get(account=self.account,302 summary2 = OpenIDRPSummary.objects.get(account=self.account,
301 trust_root=self.trust_root, openid_identifier='oid')303 trust_root=self.trust_root, openid_identifier='oid')
302 self.assertEqual(summary1, summary2)304 self.assertEqual(summary1, summary2)
303305
304 def test_increment(self):
305 summary = self.create_summary()
306 self.assertEqual(summary.total_logins, 1)
307
308 summary.increment()
309 self.assertEqual(summary.total_logins, 2)
310
311 def test_approved_data(self):306 def test_approved_data(self):
312 summary = self.create_summary()307 summary = self.create_summary()
313 summary.set_approved_data(self.approved_data)308 summary.set_approved_data(self.approved_data)
309 summary.save()
314 summary2 = OpenIDRPSummary.objects.get(account=self.account,310 summary2 = OpenIDRPSummary.objects.get(account=self.account,
315 trust_root=self.trust_root, openid_identifier='oid')311 trust_root=self.trust_root, openid_identifier='oid')
316 self.assertEqual(summary2.get_approved_data(), self.approved_data)312 self.assertEqual(summary2.get_approved_data(), self.approved_data)
317313
=== modified file 'identityprovider/tests/test_views_server.py'
--- identityprovider/tests/test_views_server.py 2011-07-27 13:00:22 +0000
+++ identityprovider/tests/test_views_server.py 2011-09-06 14:43:24 +0000
@@ -7,6 +7,7 @@
77
8from django.conf import settings8from django.conf import settings
9from django.contrib.auth.models import AnonymousUser9from django.contrib.auth.models import AnonymousUser
10from django.core.cache import cache
10from django.http import HttpRequest11from django.http import HttpRequest
11from django.test import TestCase12from django.test import TestCase
12from mock import Mock13from mock import Mock
@@ -67,6 +68,11 @@
67 self.META = {}68 self.META = {}
6869
6970
71def delete_rpconfig_cache_entry(trust_root):
72 cache.delete(OpenIDRPConfig.cache_key(trust_root))
73
74
75
70class HandleOpenIDErrorTestCase(SSOBaseTestCase):76class HandleOpenIDErrorTestCase(SSOBaseTestCase):
7177
72 # tests for the _handle_openid_error method78 # tests for the _handle_openid_error method
@@ -418,6 +424,8 @@
418 # create multiple matching OpenIDRPSummary objects424 # create multiple matching OpenIDRPSummary objects
419 account = Account.objects.get_by_email(self.login_email)425 account = Account.objects.get_by_email(self.login_email)
420 trust_root = 'http://localhost/'426 trust_root = 'http://localhost/'
427 delete_rpconfig_cache_entry(trust_root)
428
421 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,429 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,
422 openid_identifier='http://localhost/~openid1')430 openid_identifier='http://localhost/~openid1')
423 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,431 OpenIDRPSummary.objects.create(account=account, trust_root=trust_root,
@@ -427,6 +435,7 @@
427 self.assertEqual(r.status_code, 200)435 self.assertEqual(r.status_code, 200)
428 self.assertTemplateUsed(r, 'decide.html')436 self.assertTemplateUsed(r, 'decide.html')
429437
438
430 def test_decide_team_membership_with_auto_authorize(self):439 def test_decide_team_membership_with_auto_authorize(self):
431 # make sure rpconfig is set to auto authorize440 # make sure rpconfig is set to auto authorize
432 self.client.login(username='mark@example.com',441 self.client.login(username='mark@example.com',
@@ -537,10 +546,14 @@
537 password='test')546 password='test')
538547
539 # create a trusted rpconfig548 # create a trusted rpconfig
540 rpconfig = OpenIDRPConfig(trust_root='http://untrusted/',549 trust_root = 'http://untrusted/'
550 rpconfig = OpenIDRPConfig(trust_root=trust_root,
541 can_query_any_team=True,551 can_query_any_team=True,
542 description="Some description",552 description="Some description",
543 )553 )
554
555 delete_rpconfig_cache_entry("http://localhost/")
556
544 rpconfig.save()557 rpconfig.save()
545 param_overrides = {558 param_overrides = {
546 'openid.sreg.required': 'nickname,email,fullname',559 'openid.sreg.required': 'nickname,email,fullname',
@@ -1048,6 +1061,8 @@
1048 session[token] = signed.dumps(DummyORequest())1061 session[token] = signed.dumps(DummyORequest())
1049 session.save()1062 session.save()
10501063
1064 delete_rpconfig_cache_entry("http://localhost/")
1065
1051 r = self.client.get('/%s/+decide' % token)1066 r = self.client.get('/%s/+decide' % token)
1052 self.assertContains(r, '<em>not</em>')1067 self.assertContains(r, '<em>not</em>')
10531068
10541069
=== modified file 'identityprovider/tests/test_views_ui.py'
--- identityprovider/tests/test_views_ui.py 2011-08-25 13:25:51 +0000
+++ identityprovider/tests/test_views_ui.py 2011-09-06 14:43:24 +0000
@@ -522,10 +522,13 @@
522 # make sure the account is deactivated and preferred email is removed522 # make sure the account is deactivated and preferred email is removed
523 _preferred_email = account.preferredemail523 _preferred_email = account.preferredemail
524 account.status = AccountStatus.DEACTIVATED524 account.status = AccountStatus.DEACTIVATED
525 # Blow out the cached value
525 account.save()526 account.save()
526 for email_obj in account.emailaddress_set.all():527 for email_obj in account.emailaddress_set.all():
527 email_obj.status = EmailStatus.NEW528 email_obj.status = EmailStatus.NEW
528 email_obj.save()529 email_obj.save()
530
531 account = Account.objects.get_by_email('test@canonical.com')
529 self.assertEqual(account.preferredemail, None)532 self.assertEqual(account.preferredemail, None)
530533
531 r = self.client.post('/+forgot_password',534 r = self.client.post('/+forgot_password',
@@ -539,6 +542,7 @@
539 r = self.client.post(self._token_url('+resetpassword'), data)542 r = self.client.post(self._token_url('+resetpassword'), data)
540 self.assertRedirects(r, '/')543 self.assertRedirects(r, '/')
541544
545 account = Account.objects.get_by_email('test@canonical.com')
542 self.assertEqual(account.preferredemail.email, 'test@canonical.com')546 self.assertEqual(account.preferredemail.email, 'test@canonical.com')
543547
544 for email_obj in account.emailaddress_set.all():548 for email_obj in account.emailaddress_set.all():
545549
=== modified file 'identityprovider/views/server.py'
--- identityprovider/views/server.py 2011-07-27 19:20:16 +0000
+++ identityprovider/views/server.py 2011-09-06 14:43:24 +0000
@@ -504,7 +504,7 @@
504 team_names = team_names.split(',')504 team_names = team_names.split(',')
505 teams_form = TeamsRequestForm(request,505 teams_form = TeamsRequestForm(request,
506 TeamsRequest.fromOpenIDRequest(orequest),506 TeamsRequest.fromOpenIDRequest(orequest),
507 utils.get_rpconfig(orequest.trust_root))507 rpconfig)
508 approved_data['teams'] = {508 approved_data['teams'] = {
509 'requested': team_names,509 'requested': team_names,
510 'approved': teams_form.teams_approved_by_user}510 'approved': teams_form.teams_approved_by_user}
511511
=== modified file 'identityprovider/views/utils.py'
--- identityprovider/views/utils.py 2011-07-13 14:30:29 +0000
+++ identityprovider/views/utils.py 2011-09-06 14:43:24 +0000
@@ -1,7 +1,9 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3import hashlib
34
4from django.conf import settings5from django.conf import settings
6from django.core.cache import cache
57
6from identityprovider.models import OpenIDRPConfig8from identityprovider.models import OpenIDRPConfig
79
@@ -24,12 +26,19 @@
2426
2527
26def get_rpconfig(trust_root):28def get_rpconfig(trust_root):
27 alternatives = [trust_root]29 cache_key = OpenIDRPConfig.cache_key(trust_root)
2830
29 if trust_root.endswith('/'):31 default = object()
30 alternatives.append(trust_root[:-1])32 rpconfig = cache.get(cache_key, default)
31 else:33 if rpconfig is not default:
32 alternatives.append(trust_root + '/')34 return rpconfig
35
36 alternatives = [
37 trust_root,
38 trust_root[:-1] if trust_root.endswith('/') else (trust_root + '/'),
39 ]
3340
34 rpconfig = OpenIDRPConfig.objects.filter(trust_root__in=alternatives)41 rpconfig = OpenIDRPConfig.objects.filter(trust_root__in=alternatives)
35 return rpconfig and rpconfig[0] or None42 rpconfig = rpconfig and rpconfig[0] or None
43 cache.set(cache_key, rpconfig, 60 * 60 * 24)
44 return rpconfig