Fixed #26804 -- Fixed a race condition in QuerySet.update_or_create().

This commit is contained in:
Jensen Cochran 2016-06-24 12:08:42 -05:00 committed by Tim Graham
parent 76e19da5b0
commit d44afd8892
3 changed files with 62 additions and 11 deletions

View File

@ -347,6 +347,7 @@ answer newbie questions, and generally made Django that much better:
Jeff Triplett <jeff.triplett@gmail.com> Jeff Triplett <jeff.triplett@gmail.com>
Jens Diemer <django@htfx.de> Jens Diemer <django@htfx.de>
Jens Page Jens Page
Jensen Cochran <jensen.cochran@gmail.com>
Jeong-Min Lee <falsetru@gmail.com> Jeong-Min Lee <falsetru@gmail.com>
Jérémie Blaser <blaserje@gmail.com> Jérémie Blaser <blaserje@gmail.com>
Jeremy Carbaugh <jcarbaugh@gmail.com> Jeremy Carbaugh <jcarbaugh@gmail.com>

View File

@ -482,15 +482,16 @@ class QuerySet(object):
defaults = defaults or {} defaults = defaults or {}
lookup, params = self._extract_model_params(defaults, **kwargs) lookup, params = self._extract_model_params(defaults, **kwargs)
self._for_write = True self._for_write = True
try: with transaction.atomic(using=self.db):
obj = self.get(**lookup) try:
except self.model.DoesNotExist: obj = self.select_for_update().get(**lookup)
obj, created = self._create_object_from_params(lookup, params) except self.model.DoesNotExist:
if created: obj, created = self._create_object_from_params(lookup, params)
return obj, created if created:
for k, v in six.iteritems(defaults): return obj, created
setattr(obj, k, v() if callable(v) else v) for k, v in six.iteritems(defaults):
obj.save(using=self.db) setattr(obj, k, v() if callable(v) else v)
obj.save(using=self.db)
return obj, False return obj, False
def _create_object_from_params(self, lookup, params): def _create_object_from_params(self, lookup, params):

View File

@ -1,10 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import time
import traceback import traceback
from datetime import date from datetime import date, datetime, timedelta
from threading import Thread
from django.db import DatabaseError, IntegrityError from django.db import DatabaseError, IntegrityError
from django.test import TestCase, TransactionTestCase, ignore_warnings from django.test import (
TestCase, TransactionTestCase, ignore_warnings, skipUnlessDBFeature,
)
from django.utils.encoding import DjangoUnicodeDecodeError from django.utils.encoding import DjangoUnicodeDecodeError
from .models import ( from .models import (
@ -422,3 +426,48 @@ class UpdateOrCreateTests(TestCase):
) )
self.assertIs(created, False) self.assertIs(created, False)
self.assertEqual(obj.last_name, 'NotHarrison') self.assertEqual(obj.last_name, 'NotHarrison')
class UpdateOrCreateTransactionTests(TransactionTestCase):
available_apps = ['get_or_create']
@skipUnlessDBFeature('has_select_for_update')
@skipUnlessDBFeature('supports_transactions')
def test_updates_in_transaction(self):
"""
Objects are selected and updated in a transaction to avoid race
conditions. This test forces update_or_create() to hold the lock
in another thread for a relatively long time so that it can update
while it holds the lock. The updated field isn't a field in 'defaults',
so update_or_create() shouldn't have an effect on it.
"""
def birthday_sleep():
time.sleep(0.3)
return date(1940, 10, 10)
def update_birthday_slowly():
Person.objects.update_or_create(
first_name='John', defaults={'birthday': birthday_sleep}
)
Person.objects.create(first_name='John', last_name='Lennon', birthday=date(1940, 10, 9))
# update_or_create in a separate thread
t = Thread(target=update_birthday_slowly)
before_start = datetime.now()
t.start()
# Wait for lock to begin
time.sleep(0.05)
# Update during lock
Person.objects.filter(first_name='John').update(last_name='NotLennon')
after_update = datetime.now()
# Wait for thread to finish
t.join()
# The update remains and it blocked.
updated_person = Person.objects.get(first_name='John')
self.assertGreater(after_update - before_start, timedelta(seconds=0.3))
self.assertEqual(updated_person.last_name, 'NotLennon')