Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid race condition with unique checks in Django

I have a simple model:

class InvitationRequest(models.Model):
    email = models.EmailField(max_length=255, unique=True)

And a simple model form:

class InvitationRequestForm(forms.ModelForm):
    class Meta:
        model = InvitationRequest

Now, assuming that I attempt to process it in a standard way:

form = InvitationRequestForm(request.POST)
if form.is_valid():
    form.save()

There is a race condition because validation performs a simple SELECT query to determine whether or not such email is already stored, and if everything is fine then it proceeds to form.save() line. If there is a concurrent process that does the same at exactly the same moment, then both forms will validate and both processes will call form.save() thus one will succeed and the other will fail causing an IntegrityError.

What is the standard way to handle this?

I want to have a standard error in the form object so I can pass it on to the template and notify user about the problem.

I know that:

  • I can wrap everything with try/except and add new error to my form manually
  • I can wrap everything with SERIALIZABLE transaction (in MySQL as it performs next key locking fo every select then)
  • I can use override Model._perform_unique_checks and make it use select_for_update (works with MySQL because of next key locking)
  • I can acquire table-level exclusive lock

None of these solutions is appealing, also I am using PostgreSQL which differs from MySQL in this area.

like image 477
Adam Zielinski Avatar asked Sep 06 '14 17:09

Adam Zielinski


2 Answers

The standard way is to NOT handle this, as:

  1. the probability of the failure in your case is close to 0;
  2. the severity of the failure is very low.

If, for some reason, you have to be sure that the problem won't happen, you are on your own.

I haven't analyzed the sequence of events in detail but I think that using the SERIALIZABLE isolation level won't really help, it will only cause IntegrityError (or DatabaseError) to be raised in a different place.

Overriding Model._perform_unique_checks sounds to me like a bad idea, you better stay away from monkey patching if possible (and here it is possible).

As for using the table lock to prevent unlikely errors... Well, I'm not a big fan so I cannot recommend that either.

Here's a nice answer to a similar question: https://stackoverflow.com/a/3523439/176186 - I concur that catching IntegrityError and retrying is probably the easiest sane way to deal with the problem.

EDIT: I found this: Symfony2 - how to recover from unique constraint error after form submission? and I agree with @pid's answer.

like image 127
Tomasz Zieliński Avatar answered Oct 16 '22 02:10

Tomasz Zieliński


I agree with Tomasz Zielinski that common practice is to not worry about this. For most use cases it's just not worth the trouble.

If it is important, the best way is probably with optimistic concurrency. In this case it might look like (untested):

from django.forms.util import ErrorList

def handle_form(request)
    form = InvitationRequestForm(request.POST)
    try:
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(...)  # redirect to success url
    except IntegrityError:
        form._errors['email'] = ErrorList()
        form._errors['email'].append('Error msg') 

    return render(...)  # re-render the form with errors

SERIALIZABLE won't really help here. As the PostgreSQL documentation makes clear, you have to be prepared to handle serialization failures, which means that the code would look pretty much the same as above. (It would help, though, if you didn't have the unique constraint forcing the database to throw an exception.)

like image 28
Kevin Christopher Henry Avatar answered Oct 16 '22 04:10

Kevin Christopher Henry