Reputation: 4363
I know this is a classical problem, but I still don't know how to do it. On Google App Engine, I have a member registration form which uses jQuery's validation to check if a username exists. There of course is a concurrency problem: several users try to register, enter the same username, Validation finds the username available, and allow them to press "Add" at the approximately same time. Validation wouldn't detect this. In my application, username, email, and Personal ID should all be unique. How do I prevent the following code from having the concurrency problem:
member = Member()
member.username = self.request.get('username')
member.Pid = self.request.get('Pid')
member.email = self.request.get('email')
...
Upvotes: 3
Views: 436
Reputation: 63
I agreed with tesdal. If you still want to implement the memcache tric sugested by Daniel, you should do something like "memcache.add(usernameA, dummy value, short period);". So you know that usernameA is reserved for a short period and wont conflict with "memcache.add(usernameB, ..."
Upvotes: 0
Reputation: 2459
As the uniqueness constraint is on username, you have to use it as key in datastore and use transactions.
def txn():
key = ndb.Key(Member, username)
member = key.get()
if member is not None:
raise CustomAlreadyExistsException(member) # This will abort txn
member = Member(
id=username,
Pid=self.request.get('Pid'),
email=self.request.get('email'),
...)
member.put()
ndb.transaction(txn)
This makes sure only one person can register a username.
The jQuery helper would check if ndb.Key(Member, userid).get()
gives a result or not. The GET is not transactional.
To improve usability client side in "reserving" a username after checking availability, you could use memcached as suggested by Daniel, but I'd call YAGNI, skip the complexity and rather let some people get validation error after submitting the form. Note that memcached is best effort and has no guarantees about anything.
If you need guaranteed uniqueness on multiple fields, you have to add Model classes for them and check in a cross group (XG) transaction.
class Pid(ndb.Model):
member = ndb.KeyProperty()
class Email(ndb.Model):
member = ndb.KeyProperty()
class Member(ndb.Model):
pid = ndb.KeyProperty()
email = ndb.KeyProperty()
@property
def pid_value(self):
return self.pid.id()
@property
def email_value(self):
return self.email.id()
def txn():
member_key = ndb.Key(Member, username)
pid_key = ndb.Key(PersonalId, self.request.get('Pid'))
email_key = ndb.Key(Email, self.request.get('email'))
member, pid, email = ndb.get_multi([member_key, pid_key, email_key])
if member is not None or pid is not None or email is not None:
raise CustomAlreadyExistsException(member, pid, email) # This will abort txn
# Create instances referencing each other
email = Email(key=email_key, member=member_key)
pid = Pid(key=pid_key, member=member_key)
member = Member(
key=member_key,
pid=pid_key,
email=email_key,
...)
ndb.put_multi([member, pid, email])
ndb.transaction(txn, xg=True)
Upvotes: 3
Reputation: 600041
This is a great use for memcache. Your Ajax validation function should put an entry into memcache to record that the username has been requested. It should also check both memcache and the datastore to ensure that the username is free. Similarly, the registration code should check memcache to ensure that the current user is the one who requested the username.
This nicely solves your concurrency problem, and the best thing is that entries in memcache expire by themselves, either on a timed basis or when the cache gets too full.
Upvotes: 3