Merge lp:~mhall119/django-openid-auth/quick-sequence-check into lp:~django-openid-auth/django-openid-auth/trunk

Proposed by Michael Hall
Status: Merged
Approved by: Anthony Lenton
Approved revision: 89
Merged at revision: 88
Proposed branch: lp:~mhall119/django-openid-auth/quick-sequence-check
Merge into: lp:~django-openid-auth/django-openid-auth/trunk
Diff against target: 119 lines (+96/-2)
2 files modified
django_openid_auth/auth.py (+4/-2)
django_openid_auth/tests/test_views.py (+92/-0)
To merge this branch: bzr merge lp:~mhall119/django-openid-auth/quick-sequence-check
Reviewer Review Type Date Requested Status
Anthony Lenton Approve
Review via email: mp+77558@code.launchpad.net

Commit message

skip ahead to a number that is likely available when generating a sequence number to append to a duplicate username

Description of the change

Overview
========
When you have multiple duplicates of username, the process of incrementing the appended number starting from i=0 can become quite time consuming and allows a race condition

Details
=======
Where there are many duplicates of a username, especially when using 'openiduser' for logins without a nickname, making a db call from 1 to n to find an available username causes a delay which allows the user to attempt the login again. Because we create the django.contrib.auth User first and then associate it with an openid, this duplicate request will create a duplicate User record but will then fail when assigning it to an openid.

This patch should drastically reduce the time it takes to identify an available username and closes the window where this race condition can occur.

To post a comment you must log in.
89. By Michael Hall

Add test cases for optimized unique username sequence generation

Revision history for this message
Anthony Lenton (elachuni) wrote :

Thanks for the fix Michael!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_openid_auth/auth.py'
2--- django_openid_auth/auth.py 2011-09-12 13:50:50 +0000
3+++ django_openid_auth/auth.py 2011-09-29 17:40:28 +0000
4@@ -198,8 +198,10 @@
5 "already in use for a different account." % nickname)
6
7 # Pick a username for the user based on their nickname,
8- # checking for conflicts.
9- i = 1
10+ # checking for conflicts. Start with number of existing users who's
11+ # username starts with this nickname to avoid having to iterate over
12+ # all of the existing ones.
13+ i = User.objects.filter(username__startswith=nickname).count() + 1
14 while True:
15 username = nickname
16 if i > 1:
17
18=== modified file 'django_openid_auth/tests/test_views.py'
19--- django_openid_auth/tests/test_views.py 2011-09-12 13:50:50 +0000
20+++ django_openid_auth/tests/test_views.py 2011-09-29 17:40:28 +0000
21@@ -569,6 +569,98 @@
22 self.assertEquals(user.last_name, 'User')
23 self.assertEquals(user.email, 'foo@example.com')
24
25+ def test_login_duplicate_username_numbering(self):
26+ settings.OPENID_FOLLOW_RENAMES = False
27+ settings.OPENID_CREATE_USERS = True
28+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
29+ # Setup existing user who's name we're going to conflict with
30+ user = User.objects.create_user('testuser', 'someone@example.com')
31+
32+ # identity url is for 'renameuser'
33+ openid_req = {'openid_identifier': 'http://example.com/identity',
34+ 'next': '/getuser/'}
35+ # but returned username is for 'testuser', which already exists for another identity
36+ openid_resp = {'nickname': 'testuser', 'fullname': 'Test User',
37+ 'email': 'test@example.com'}
38+ self._do_user_login(openid_req, openid_resp)
39+ response = self.client.get('/getuser/')
40+
41+ # Since this username is already taken by someone else, we go through
42+ # the process of adding +i to it, and get testuser2.
43+ self.assertEquals(response.content, 'testuser2')
44+
45+ def test_login_duplicate_username_numbering_with_conflicts(self):
46+ settings.OPENID_FOLLOW_RENAMES = False
47+ settings.OPENID_CREATE_USERS = True
48+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
49+ # Setup existing user who's name we're going to conflict with
50+ user = User.objects.create_user('testuser', 'someone@example.com')
51+ user = User.objects.create_user('testuser3', 'someone@example.com')
52+
53+ # identity url is for 'renameuser'
54+ openid_req = {'openid_identifier': 'http://example.com/identity',
55+ 'next': '/getuser/'}
56+ # but returned username is for 'testuser', which already exists for another identity
57+ openid_resp = {'nickname': 'testuser', 'fullname': 'Test User',
58+ 'email': 'test@example.com'}
59+ self._do_user_login(openid_req, openid_resp)
60+ response = self.client.get('/getuser/')
61+
62+ # Since this username is already taken by someone else, we go through
63+ # the process of adding +i to it starting with the count of users with
64+ # username starting with 'testuser', of which there are 2. i should
65+ # start at 3, which already exists, so it should skip to 4.
66+ self.assertEquals(response.content, 'testuser4')
67+
68+ def test_login_duplicate_username_numbering_with_holes(self):
69+ settings.OPENID_FOLLOW_RENAMES = False
70+ settings.OPENID_CREATE_USERS = True
71+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
72+ # Setup existing user who's name we're going to conflict with
73+ user = User.objects.create_user('testuser', 'someone@example.com')
74+ user = User.objects.create_user('testuser1', 'someone@example.com')
75+ user = User.objects.create_user('testuser6', 'someone@example.com')
76+ user = User.objects.create_user('testuser7', 'someone@example.com')
77+ user = User.objects.create_user('testuser8', 'someone@example.com')
78+
79+ # identity url is for 'renameuser'
80+ openid_req = {'openid_identifier': 'http://example.com/identity',
81+ 'next': '/getuser/'}
82+ # but returned username is for 'testuser', which already exists for another identity
83+ openid_resp = {'nickname': 'testuser', 'fullname': 'Test User',
84+ 'email': 'test@example.com'}
85+ self._do_user_login(openid_req, openid_resp)
86+ response = self.client.get('/getuser/')
87+
88+ # Since this username is already taken by someone else, we go through
89+ # the process of adding +i to it starting with the count of users with
90+ # username starting with 'testuser', of which there are 5. i should
91+ # start at 6, and increment until it reaches 9.
92+ self.assertEquals(response.content, 'testuser9')
93+
94+ def test_login_duplicate_username_numbering_with_nonsequential_matches(self):
95+ settings.OPENID_FOLLOW_RENAMES = False
96+ settings.OPENID_CREATE_USERS = True
97+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
98+ # Setup existing user who's name we're going to conflict with
99+ user = User.objects.create_user('testuser', 'someone@example.com')
100+ user = User.objects.create_user('testuserfoo', 'someone@example.com')
101+
102+ # identity url is for 'renameuser'
103+ openid_req = {'openid_identifier': 'http://example.com/identity',
104+ 'next': '/getuser/'}
105+ # but returned username is for 'testuser', which already exists for another identity
106+ openid_resp = {'nickname': 'testuser', 'fullname': 'Test User',
107+ 'email': 'test@example.com'}
108+ self._do_user_login(openid_req, openid_resp)
109+ response = self.client.get('/getuser/')
110+
111+ # Since this username is already taken by someone else, we go through
112+ # the process of adding +i to it starting with the count of users with
113+ # username starting with 'testuser', of which there are 2. i should
114+ # start at 3, which will be available.
115+ self.assertEquals(response.content, 'testuser3')
116+
117 def test_login_follow_rename(self):
118 settings.OPENID_FOLLOW_RENAMES = True
119 settings.OPENID_UPDATE_DETAILS_FROM_SREG = True

Subscribers

People subscribed via source and target branches