#29499: Race condition in QuerySet.update_or_create()
-------------------------------------+-------------------------------------
               Reporter:  Michael    |          Owner:  nobody
  Sanders                            |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  2.0
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:  race-condition
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 I believe that there is a potential race condition in
 `QuerySet.update_or_create()`

 When initially trying to obtain the object to update using `get()`, the
 row is locked using the `select_for_update()` method - this would lock the
 object against changes by other processes until the end of the
 transaction. However, if the object does not already exist,
 `update_or_create()` then calls the `_create_object_from_params()` method
 - see code:

   {{{#!python
         with transaction.atomic(using=self.db):
             try:
                 obj = self.select_for_update().get(**lookup)
             except self.model.DoesNotExist:
                 obj, created = self._create_object_from_params(lookup,
 params)
    }}}

 The `_create_object_from_params()` method looks like this:
    {{{#!python
     def _create_object_from_params(self, lookup, params):
         """
         Try to create an object using passed params. Used by
 get_or_create()
         and update_or_create().
         """
         try:
             with transaction.atomic(using=self.db):
                 params = {k: v() if callable(v) else v for k, v in
 params.items()}
                 obj = self.create(**params)
             return obj, True
         except IntegrityError as e:
             try:
                 return self.get(**lookup), False
             except self.model.DoesNotExist:
                 pass
             raise e
 }}}

 Here, it initially tries to create the object. However (assuming that the
 object has a unique key constraint policed by the DB), if the object has
 already been created meanwhile (by a different process) the
 `IntegrityError` is caught and the code uses the `get()` method to attempt
 to obtain the newly created object. The object is then returned to the
 calling `update_or_create()` method to update - however in this case, the
 row has not been locked against changes. So, at this point other processes
 are free to modify the DB row and those changes might then be overwritten
 by the `save()` in `update_or_create()`.

 == Suggested Fix ==
 This could be fixed by changing the `_create_object_from_params()` method
 to take a new parameter to specify whether the object should be locked on
 get, i.e.:

    {{{#!python
     def _create_object_from_params(self, lookup, params, lock=False):
         """
         Try to create an object using passed params. Used by
 get_or_create()
         and update_or_create().
         """
         try:
             with transaction.atomic(using=self.db):
                 params = {k: v() if callable(v) else v for k, v in
 params.items()}
                 obj = self.create(**params)
             return obj, True
         except IntegrityError as e:
             try:
                 if lock:
                     return self.select_for_update().get(**lookup), False
                 else:
                     return self.get(**lookup), False
             except self.model.DoesNotExist:
                 pass
             raise e
 }}}

 The call to `_create_object_from_params()` from `get_or_create()` would
 remain the same, but from `update_or_create()` it would change to:
    {{{#!python
                 obj, created = self._create_object_from_params(lookup,
 params, lock=True)
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/29499>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/055.3116d6d59d8539ce5bb1ab3bf1170f2e%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to