mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #26804 -- Fixed a race condition in QuerySet.update_or_create().
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							76e19da5b0
						
					
				
				
					commit
					d44afd8892
				
			
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -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> | ||||||
|   | |||||||
| @@ -482,8 +482,9 @@ 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 | ||||||
|  |         with transaction.atomic(using=self.db): | ||||||
|             try: |             try: | ||||||
|             obj = self.get(**lookup) |                 obj = self.select_for_update().get(**lookup) | ||||||
|             except self.model.DoesNotExist: |             except self.model.DoesNotExist: | ||||||
|                 obj, created = self._create_object_from_params(lookup, params) |                 obj, created = self._create_object_from_params(lookup, params) | ||||||
|                 if created: |                 if created: | ||||||
|   | |||||||
| @@ -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') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user