Ticket #2705: for_update-1.0.1-v2.diff
File for_update-1.0.1-v2.diff, 18.7 KB (added by , 16 years ago) |
---|
-
django/db/models/sql/query.py
12 12 from django.utils.tree import Node 13 13 from django.utils.datastructures import SortedDict 14 14 from django.utils.encoding import force_unicode 15 from django.db import connection 15 from django.db import connection, DatabaseError 16 16 from django.db.models import signals 17 17 from django.db.models.fields import FieldDoesNotExist 18 18 from django.db.models.query_utils import select_related_descend … … 27 27 except NameError: 28 28 from sets import Set as set # Python 2.3 fallback 29 29 30 __all__ = ['Query', 'BaseQuery' ]30 __all__ = ['Query', 'BaseQuery', 'LockNotAvailable'] 31 31 32 class LockNotAvailable(DatabaseError): 33 ''' 34 Raised when a query fails because a lock was not available. 35 ''' 36 pass 37 32 38 class BaseQuery(object): 33 39 """ 34 40 A single SQL query. … … 71 77 self.order_by = [] 72 78 self.low_mark, self.high_mark = 0, None # Used for offset/limit 73 79 self.distinct = False 80 self.select_for_update = False 81 self.select_for_update_nowait = False 74 82 self.select_related = False 75 83 self.related_select_cols = [] 76 84 … … 179 187 obj.order_by = self.order_by[:] 180 188 obj.low_mark, obj.high_mark = self.low_mark, self.high_mark 181 189 obj.distinct = self.distinct 190 obj.select_for_update = self.select_for_update 191 obj.select_for_update_nowait = self.select_for_update_nowait 182 192 obj.select_related = self.select_related 183 193 obj.related_select_cols = [] 184 194 obj.max_depth = self.max_depth … … 225 235 obj = self.clone() 226 236 obj.clear_ordering(True) 227 237 obj.clear_limits() 238 obj.select_for_update = False 228 239 obj.select_related = False 229 240 obj.related_select_cols = [] 230 241 obj.related_select_fields = [] … … 310 321 result.append('LIMIT %d' % val) 311 322 result.append('OFFSET %d' % self.low_mark) 312 323 324 if self.select_for_update and self.connection.features.has_select_for_update: 325 nowait = self.select_for_update_nowait and self.connection.features.has_select_for_update 326 result.append("%s" % self.connection.ops.for_update_sql(nowait=nowait)) 327 313 328 params.extend(self.extra_params) 314 329 return ' '.join(result), tuple(params) 315 330 … … 1731 1746 return 1732 1747 1733 1748 cursor = self.connection.cursor() 1734 cursor.execute(sql, params) 1749 try: 1750 cursor.execute(sql, params) 1751 except DatabaseError, e: 1752 if self.connection.features.has_select_for_update_nowait and self.connection.ops.signals_lock_not_available(e): 1753 raise LockNotAvailable(*e.args) 1754 raise 1735 1755 1736 1756 if not result_type: 1737 1757 return cursor -
django/db/models/manager.py
119 119 def order_by(self, *args, **kwargs): 120 120 return self.get_query_set().order_by(*args, **kwargs) 121 121 122 def select_for_update(self, *args, **kwargs): 123 return self.get_query_set().select_for_update(*args, **kwargs) 124 122 125 def select_related(self, *args, **kwargs): 123 126 return self.get_query_set().select_related(*args, **kwargs) 124 127 -
django/db/models/__init__.py
9 9 from django.db.models.fields.subclassing import SubfieldBase 10 10 from django.db.models.fields.files import FileField, ImageField 11 11 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel 12 from django.db.models.sql.query import LockNotAvailable 12 13 from django.db.models import signals 13 14 14 15 # Admin stages. -
django/db/models/query.py
385 385 del_query = self._clone() 386 386 387 387 # Disable non-supported fields. 388 del_query.query.select_for_update = False 388 389 del_query.query.select_related = False 389 390 del_query.query.clear_ordering() 390 391 … … 524 525 else: 525 526 return self._filter_or_exclude(None, **filter_obj) 526 527 528 def select_for_update(self, **kwargs): 529 """ 530 Returns a new QuerySet instance that will select objects with a 531 FOR UPDATE lock. 532 """ 533 # Default to false for nowait 534 nowait = kwargs.pop('nowait', False) 535 obj = self._clone() 536 obj.query.select_for_update = True 537 obj.query.select_for_update_nowait = nowait 538 return obj 539 527 540 def select_related(self, *fields, **kwargs): 528 541 """ 529 542 Returns a new QuerySet instance that will select related objects. -
django/db/backends/mysql/base.py
22 22 raise ImproperlyConfigured("MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__) 23 23 24 24 from MySQLdb.converters import conversions 25 from MySQLdb.constants import FIELD_TYPE, FLAG 25 from MySQLdb.constants import FIELD_TYPE, FLAG, ER 26 26 27 27 from django.db.backends import * 28 28 from django.db.backends.mysql.client import DatabaseClient … … 111 111 empty_fetchmany_value = () 112 112 update_can_self_select = False 113 113 related_fields_match_type = True 114 has_select_for_update = True 115 has_select_for_update_nowait = False 114 116 115 117 class DatabaseOperations(BaseDatabaseOperations): 116 118 def date_extract_sql(self, lookup_type, field_name): … … 193 195 # MySQL doesn't support microseconds 194 196 return unicode(value.replace(microsecond=0)) 195 197 198 signals_deadlock = lambda self, e: e.args[0] == ER.LOCK_DEADLOCK 199 196 200 def year_lookup_bounds(self, value): 197 201 # Again, no microseconds 198 202 first = '%s-01-01 00:00:00' -
django/db/backends/__init__.py
74 74 # If True, don't use integer foreign keys referring to, e.g., positive 75 75 # integer primary keys. 76 76 related_fields_match_type = False 77 has_select_for_update = False 78 has_select_for_update_nowait = False 77 79 78 80 class BaseDatabaseOperations(object): 79 81 """ … … 143 145 """ 144 146 return '%s' 145 147 148 def for_update_sql(self, nowait=False): 149 """ 150 Return FOR UPDATE SQL clause to lock row for update 151 """ 152 if nowait: 153 nowaitstr = ' NOWAIT' 154 else: 155 nowaitstr = '' 156 return 'FOR UPDATE' + nowaitstr 157 146 158 def fulltext_search_sql(self, field_name): 147 159 """ 148 160 Returns the SQL WHERE clause to use in order to perform a full-text -
django/db/backends/postgresql_psycopg2/base.py
15 15 try: 16 16 import psycopg2 as Database 17 17 import psycopg2.extensions 18 from psycopg2 import errorcodes 18 19 except ImportError, e: 19 20 from django.core.exceptions import ImproperlyConfigured 20 21 raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) … … 29 30 class DatabaseFeatures(BaseDatabaseFeatures): 30 31 needs_datetime_string_cast = False 31 32 uses_savepoints = True 33 has_select_for_update = True 34 has_select_for_update_nowait = True 32 35 33 36 class DatabaseOperations(PostgresqlDatabaseOperations): 34 37 def last_executed_query(self, cursor, sql, params): … … 37 40 # http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query 38 41 return cursor.query 39 42 43 signals_deadlock = lambda self, e: e.pgcode == errorcodes.DEADLOCK_DETECTED 44 45 signals_lock_not_available = lambda self, e: e.pgcode == errorcodes.LOCK_NOT_AVAILABLE 46 47 40 48 class DatabaseWrapper(BaseDatabaseWrapper): 41 49 operators = { 42 50 'exact': '= %s', -
django/views/decorators/deadlock.py
1 """ 2 Decorators for deadlock handling. 3 """ 4 import sys 5 try: 6 from functools import wraps 7 except ImportError: 8 from django.utils.functional import wraps # Python 2.3, 2.4 fallback. 9 10 from django.db import transaction, connection, DatabaseError 11 12 class DeadlockError(Exception): 13 """ 14 Thrown by a view decorated by handle_deadlock(max_retries) when a deadlock 15 has been detected and the view won't be called again to retry the aborted 16 transaction. 17 """ 18 pass 19 20 def handle_deadlocks(max_retries=2): 21 """ 22 Decorator to retry a view when a database deadlock is detected. 23 24 When there are no retries left, raises DeadlockError with the traceback of 25 the original, database backend-specific exception. 26 27 Views using querysets constructed with select_for_update() should use this 28 decorator. If the backend does not support locking and/or deadlock 29 handling, this doesn't do anything. 30 """ 31 if connection.features.has_select_for_update: 32 signals_deadlock = connection.ops.signals_deadlock 33 else: 34 return lambda f: f 35 def decorator(func): 36 def inner(*args, **kwargs): 37 retries = 0 38 while 1: 39 try: 40 return func(*args, **kwargs) 41 except DatabaseError, e: 42 if signals_deadlock(e): 43 if retries == max_retries: 44 raise DeadlockError, 'Deadlock detected', sys.exc_info()[2] 45 retries += 1 46 # Rollback needed by PostgreSQL 47 transaction.rollback() 48 transaction.set_clean() 49 continue 50 raise 51 return wraps(func)(inner) 52 return decorator 53 -
tests/regressiontests/select_for_update/__init__.py
1 2 -
tests/regressiontests/select_for_update/tests.py
1 import time 2 import threading 3 from unittest import TestCase 4 5 from django.conf import settings 6 from django.db import transaction, connection 7 from django.db.models import LockNotAvailable 8 from django.views.decorators.deadlock import DeadlockError, handle_deadlocks 9 10 from regressiontests.select_for_update.models import Tag 11 12 class SelectForUpdateTests(TestCase): 13 14 def setUp(self): 15 Tag.objects.create(name='1') 16 Tag.objects.create(name='2') 17 18 def test_basics(self): 19 def test(): 20 t = Tag(name='update') 21 t.save() 22 transaction.commit() 23 tfound = Tag.objects.select_for_update().get(pk=t.id) 24 tfound.name = 'update2' 25 tfound.save() 26 transaction.commit() 27 tfound = Tag.objects.select_for_update().get(pk=t.id) 28 tfound.delete() 29 transaction.commit() 30 test = transaction.commit_manually(test) 31 test() 32 33 def test_backend_features(self): 34 if settings.DATABASE_ENGINE == 'postgresql_psycopg2': 35 self.failUnless(hasattr(connection.ops, 'signals_deadlock')) 36 self.failUnless(hasattr(connection.ops, 'signals_lock_not_available')) 37 elif settings.DATABASE_ENGINE == 'mysql': 38 self.failUnless(hasattr(connection.ops, 'signals_deadlock')) 39 40 def test_deadlock(self): 41 ''' 42 This test will fail on MySQL if the storage engine is not InnoDB. 43 ''' 44 # Don't look for deadlocks if the backend doesn't support SELECT FOR UPDATE 45 if not connection.features.has_select_for_update: 46 return 47 def test(max_retries): 48 vars = {0: None, 1: None} 49 def view0(): 50 t1 = Tag.objects.select_for_update().get(pk=1) 51 time.sleep(1) 52 t2 = Tag.objects.select_for_update().get(pk=2) 53 transaction.commit() 54 view0 = handle_deadlocks(max_retries=max_retries)(transaction.commit_manually(view0)) 55 def view1(): 56 t2 = Tag.objects.select_for_update().get(pk=2) 57 time.sleep(1) 58 t1 = Tag.objects.select_for_update().get(pk=1) 59 transaction.commit() 60 view1 = handle_deadlocks(max_retries=max_retries)(transaction.commit_manually(view1)) 61 def thread0(vars): 62 try: 63 view0() 64 except Exception, e: 65 vars[0] = e 66 def thread1(vars): 67 try: 68 view1() 69 except Exception, e: 70 vars[1] = e 71 t0 = threading.Thread(target=thread0, args=(vars,)) 72 t1 = threading.Thread(target=thread1, args=(vars,)) 73 t0.start() 74 t1.start() 75 t0.join() 76 t1.join() 77 return vars[0], vars[1] 78 # Make a deadlock and don't retry the aborted transaction 79 # We are expecting a DeadlockError 80 e0, e1 = test(0) 81 self.assertEqual(e0 or e1, e1 or e0) 82 self.assert_(isinstance(e0 or e1, DeadlockError)) 83 # Make a deadlock and retry the aborted transaction 84 # We expect no errors 85 e0, e1 = test(1) 86 self.assertEqual(e0 or e1, None) 87 88 def test_nowait(self): 89 if not connection.features.has_select_for_update_nowait: 90 return 91 def view(): 92 try: 93 t1 = Tag.objects.select_for_update(nowait=True).get(pk=1) 94 time.sleep(1) 95 finally: 96 transaction.rollback() 97 view = transaction.commit_manually(view) 98 t = threading.Thread(target=view) 99 t.start() 100 time.sleep(.25) 101 try: 102 view() 103 except LockNotAvailable: 104 pass 105 else: 106 self.fail('Expected view to raise LockNotAvailable') 107 -
tests/regressiontests/select_for_update/models.py
1 from django.db import models 2 3 class Tag(models.Model): 4 name = models.CharField(max_length=10) 5 parent = models.ForeignKey('self', blank=True, null=True, 6 related_name='children') 7 8 class Meta: 9 ordering = ['name'] 10 11 def __unicode__(self): 12 return self.name 13 -
docs/ref/models/querysets.txt
726 726 727 727 Entry.objects.extra(where=['headline=%s'], params=['Lennon']) 728 728 729 ``select_for_update(nowait=False)`` 730 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 731 732 Returns a queryset that will lock rows until the end of the transaction, 733 generating a SELECT ... FOR UPDATE statement on supported databases. 734 735 For example:: 736 737 entries = Entry.objects.select_for_update().filter(author=request.user) 738 739 All matched entries will be locked until the end of the transaction block, 740 meaning that other transactions will be prevented from changing or acquiring 741 locks on them. 742 743 Usually, if another transaction has already acquired a lock on one of the 744 selected rows, the query will block until the lock is released. If this is 745 not the behaviour you want, call ``select_for_update(nowait=True)``. This will 746 make the call non-blocking. If a conflicting lock is already acquired by 747 another transaction, ``django.db.models.LockNotAvailable`` will be raised when 748 the queryset is evaluated. 749 750 Using blocking locks on a database can lead to deadlocks. This occurs when two 751 concurrent transactions are both waiting on a lock the other transaction 752 already holds. To deal with deadlocks, wrap your views that use 753 ``select_for_update(nowait=False)`` with the 754 ``django.views.decorators.deadlock.handle_deadlocks`` decorator. 755 756 For example:: 757 758 from django.db import transaction 759 from django.views.decorators.deadlock import handle_deadlocks 760 761 @handle_deadlocks(max_retries=2) 762 @transaction.commit_on_success 763 def my_view(request): 764 ... 765 766 If the database engine detects a deadlock involving ``my_view`` and decides 767 to abort its transaction, it will be automatically retried. If deadlocks keep 768 occurring after two repeated attempts, 769 ``django.views.decorators.DeadlockError`` will be raised, which can be 770 propagated to the user or handled in a middleware. 771 772 Currently the ``postgresql_psycopg2`` and ``mysql`` database backends 773 support ``select_for_update()`` but MySQL has no support for the ``nowait`` 774 argument. Other backends will simply generate queries as if 775 ``select_for_update()`` had not been used. 776 729 777 QuerySet methods that do not return QuerySets 730 778 --------------------------------------------- 731 779 -
docs/ref/databases.txt
244 244 matter unless you're printing out the field values and are expecting to see 245 245 ``True`` and ``False.``. 246 246 247 Row locking with ``QuerySet.select_for_update()`` 248 ------------------------------------------------- 249 250 MySQL does not support the NOWAIT option to the SELECT ... FOR UPDATE 251 statement. However, you may call the ``select_for_update()`` method of a 252 queryset with ``nowait=True``. In that case, the argument will be silently 253 discarded and the generated query will block until the requested lock can be 254 acquired. 255 247 256 .. _sqlite-notes: 248 257 249 258 SQLite notes