Code

Ticket #2705: for_update_9962.diff

File for_update_9962.diff, 12.6 KB (added by anih, 5 years ago)

for update patch with transaction.commit_unless_managed() inside select_for_update()

Line 
1Index: django/db/models/sql/query.py
2===================================================================
3--- django/db/models/sql/query.py       (wersja 9962)
4+++ django/db/models/sql/query.py       (kopia robocza)
5@@ -13,7 +13,7 @@
6 from django.utils.datastructures import SortedDict
7 from django.utils.encoding import force_unicode
8 from django.db.backends.util import truncate_name
9-from django.db import connection
10+from django.db import connection, DatabaseError
11 from django.db.models import signals
12 from django.db.models.fields import FieldDoesNotExist
13 from django.db.models.query_utils import select_related_descend
14@@ -29,8 +29,14 @@
15 except NameError:
16     from sets import Set as set     # Python 2.3 fallback
17 
18-__all__ = ['Query', 'BaseQuery']
19+__all__ = ['Query', 'BaseQuery', 'LockNotAvailable']
20 
21+class LockNotAvailable(DatabaseError):
22+    '''
23+    Raised when a query fails because a lock was not available.
24+    '''
25+    pass
26+
27 class BaseQuery(object):
28     """
29     A single SQL query.
30@@ -73,6 +79,8 @@
31         self.order_by = []
32         self.low_mark, self.high_mark = 0, None  # Used for offset/limit
33         self.distinct = False
34+        self.select_for_update = False
35+        self.select_for_update_nowait = False
36         self.select_related = False
37         self.related_select_cols = []
38 
39@@ -187,6 +195,8 @@
40         obj.order_by = self.order_by[:]
41         obj.low_mark, obj.high_mark = self.low_mark, self.high_mark
42         obj.distinct = self.distinct
43+        obj.select_for_update = self.select_for_update
44+        obj.select_for_update_nowait = self.select_for_update_nowait
45         obj.select_related = self.select_related
46         obj.related_select_cols = []
47         obj.aggregates = self.aggregates.copy()
48@@ -307,6 +317,7 @@
49 
50         query.clear_ordering(True)
51         query.clear_limits()
52+        query.select_for_update = False
53         query.select_related = False
54         query.related_select_cols = []
55         query.related_select_fields = []
56@@ -425,6 +436,10 @@
57                         result.append('LIMIT %d' % val)
58                 result.append('OFFSET %d' % self.low_mark)
59 
60+        if self.select_for_update and self.connection.features.has_select_for_update:
61+            nowait = self.select_for_update_nowait and self.connection.features.has_select_for_update
62+            result.append("%s" % self.connection.ops.for_update_sql(nowait=nowait))
63+
64         params.extend(self.extra_params)
65         return ' '.join(result), tuple(params)
66 
67@@ -2028,7 +2043,12 @@
68             else:
69                 return
70         cursor = self.connection.cursor()
71-        cursor.execute(sql, params)
72+        try:
73+            cursor.execute(sql, params)
74+        except DatabaseError, e:
75+            if self.connection.features.has_select_for_update_nowait and self.connection.ops.signals_lock_not_available(e):
76+                raise LockNotAvailable(*e.args)
77+            raise
78 
79         if not result_type:
80             return cursor
81Index: django/db/models/manager.py
82===================================================================
83--- django/db/models/manager.py (wersja 9962)
84+++ django/db/models/manager.py (kopia robocza)
85@@ -125,6 +125,9 @@
86     def order_by(self, *args, **kwargs):
87         return self.get_query_set().order_by(*args, **kwargs)
88 
89+    def select_for_update(self, *args, **kwargs):
90+        return self.get_query_set().select_for_update(*args, **kwargs)
91+       
92     def select_related(self, *args, **kwargs):
93         return self.get_query_set().select_related(*args, **kwargs)
94 
95Index: django/db/models/__init__.py
96===================================================================
97--- django/db/models/__init__.py        (wersja 9962)
98+++ django/db/models/__init__.py        (kopia robocza)
99@@ -11,6 +11,7 @@
100 from django.db.models.fields.subclassing import SubfieldBase
101 from django.db.models.fields.files import FileField, ImageField
102 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
103+from django.db.models.sql.query import LockNotAvailable
104 from django.db.models import signals
105 
106 # Admin stages.
107Index: django/db/models/query.py
108===================================================================
109--- django/db/models/query.py   (wersja 9962)
110+++ django/db/models/query.py   (kopia robocza)
111@@ -418,6 +418,7 @@
112         del_query = self._clone()
113 
114         # Disable non-supported fields.
115+        del_query.query.select_for_update = False
116         del_query.query.select_related = False
117         del_query.query.clear_ordering()
118 
119@@ -557,6 +558,19 @@
120         else:
121             return self._filter_or_exclude(None, **filter_obj)
122 
123+    def select_for_update(self, **kwargs):
124+        """
125+        Returns a new QuerySet instance that will select objects with a
126+        FOR UPDATE lock.
127+        """
128+        # Default to false for nowait
129+        nowait = kwargs.pop('nowait', False)
130+        obj = self._clone()
131+        obj.query.select_for_update = True
132+        obj.query.select_for_update_nowait = nowait
133+        transaction.commit_unless_managed()
134+        return obj
135+
136     def select_related(self, *fields, **kwargs):
137         """
138         Returns a new QuerySet instance that will select related objects.
139Index: django/db/backends/mysql/base.py
140===================================================================
141--- django/db/backends/mysql/base.py    (wersja 9962)
142+++ django/db/backends/mysql/base.py    (kopia robocza)
143@@ -22,7 +22,7 @@
144     raise ImproperlyConfigured("MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__)
145 
146 from MySQLdb.converters import conversions
147-from MySQLdb.constants import FIELD_TYPE, FLAG
148+from MySQLdb.constants import FIELD_TYPE, FLAG, ER
149 
150 from django.db.backends import *
151 from django.db.backends.mysql.client import DatabaseClient
152@@ -112,6 +112,8 @@
153     update_can_self_select = False
154     allows_group_by_pk = True
155     related_fields_match_type = True
156+    has_select_for_update = True
157+    has_select_for_update_nowait = False
158 
159 class DatabaseOperations(BaseDatabaseOperations):
160     def date_extract_sql(self, lookup_type, field_name):
161@@ -207,6 +209,8 @@
162         # MySQL doesn't support microseconds
163         return unicode(value.replace(microsecond=0))
164 
165+    signals_deadlock = lambda self, e: e.args[0] == ER.LOCK_DEADLOCK
166+
167     def year_lookup_bounds(self, value):
168         # Again, no microseconds
169         first = '%s-01-01 00:00:00'
170Index: django/db/backends/oracle/base.py
171===================================================================
172--- django/db/backends/oracle/base.py   (wersja 9962)
173+++ django/db/backends/oracle/base.py   (kopia robocza)
174@@ -36,6 +36,8 @@
175     needs_datetime_string_cast = False
176     uses_custom_query_class = True
177     interprets_empty_strings_as_nulls = True
178+    has_select_for_update = True
179+    has_select_for_update_nowait = True
180 
181 
182 class DatabaseOperations(BaseDatabaseOperations):
183@@ -201,6 +203,12 @@
184                                        'column': column_name})
185         return output
186 
187+    def signals_deadlock(self, exception):
188+        return exception.args[0].code == 60
189+
190+    def signals_lock_not_available(self, exception):
191+        return exception.args[0].code == 54
192+
193     def start_transaction_sql(self):
194         return ''
195 
196Index: django/db/backends/__init__.py
197===================================================================
198--- django/db/backends/__init__.py      (wersja 9962)
199+++ django/db/backends/__init__.py      (kopia robocza)
200@@ -81,6 +81,8 @@
201     # If True, don't use integer foreign keys referring to, e.g., positive
202     # integer primary keys.
203     related_fields_match_type = False
204+    has_select_for_update = False
205+    has_select_for_update_nowait = False
206 
207 class BaseDatabaseOperations(object):
208     """
209@@ -158,6 +160,16 @@
210         """
211         return []
212 
213+    def for_update_sql(self, nowait=False):
214+        """
215+        Return FOR UPDATE SQL clause to lock row for update
216+        """
217+        if nowait:
218+            nowaitstr = ' NOWAIT'
219+        else:
220+            nowaitstr = ''
221+        return 'FOR UPDATE' + nowaitstr
222+
223     def fulltext_search_sql(self, field_name):
224         """
225         Returns the SQL WHERE clause to use in order to perform a full-text
226Index: django/db/backends/postgresql_psycopg2/base.py
227===================================================================
228--- django/db/backends/postgresql_psycopg2/base.py      (wersja 9962)
229+++ django/db/backends/postgresql_psycopg2/base.py      (kopia robocza)
230@@ -15,6 +15,7 @@
231 try:
232     import psycopg2 as Database
233     import psycopg2.extensions
234+    from psycopg2 import errorcodes
235 except ImportError, e:
236     from django.core.exceptions import ImproperlyConfigured
237     raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
238@@ -29,6 +30,8 @@
239 class DatabaseFeatures(BaseDatabaseFeatures):
240     needs_datetime_string_cast = False
241     uses_savepoints = True
242+    has_select_for_update = True
243+    has_select_for_update_nowait = True
244 
245 class DatabaseOperations(PostgresqlDatabaseOperations):
246     def last_executed_query(self, cursor, sql, params):
247@@ -37,6 +40,11 @@
248         # http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query
249         return cursor.query
250 
251+    signals_deadlock = lambda self, e: e.pgcode == errorcodes.DEADLOCK_DETECTED
252+
253+    signals_lock_not_available = lambda self, e: e.pgcode == errorcodes.LOCK_NOT_AVAILABLE
254+   
255+
256 class DatabaseWrapper(BaseDatabaseWrapper):
257     operators = {
258         'exact': '= %s',
259Index: docs/ref/models/querysets.txt
260===================================================================
261--- docs/ref/models/querysets.txt       (wersja 9962)
262+++ docs/ref/models/querysets.txt       (kopia robocza)
263@@ -768,6 +768,54 @@
264 
265         Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
266 
267+``select_for_update(nowait=False)``
268+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
269+
270+Returns a queryset that will lock rows until the end of the transaction,
271+generating a SELECT ... FOR UPDATE statement on supported databases.
272+
273+For example::
274+
275+    entries = Entry.objects.select_for_update().filter(author=request.user)
276+
277+All matched entries will be locked until the end of the transaction block,
278+meaning that other transactions will be prevented from changing or acquiring
279+locks on them.
280+
281+Usually, if another transaction has already acquired a lock on one of the
282+selected rows, the query will block until the lock is released. If this is
283+not the behaviour you want, call ``select_for_update(nowait=True)``. This will
284+make the call non-blocking. If a conflicting lock is already acquired by
285+another transaction, ``django.db.models.LockNotAvailable`` will be raised when
286+the queryset is evaluated.
287+
288+Using blocking locks on a database can lead to deadlocks. This occurs when two
289+concurrent transactions are both waiting on a lock the other transaction
290+already holds. To deal with deadlocks, wrap your views that use
291+``select_for_update(nowait=False)`` with the
292+``django.views.decorators.deadlock.handle_deadlocks`` decorator.
293+
294+For example::
295+
296+    from django.db import transaction
297+    from django.views.decorators.deadlock import handle_deadlocks
298+
299+    @handle_deadlocks(max_retries=2)
300+    @transaction.commit_on_success
301+    def my_view(request):
302+        ...
303+
304+If the database engine detects a deadlock involving ``my_view`` and decides
305+to abort its transaction, it will be automatically retried. If deadlocks keep
306+occurring after two repeated attempts,
307+``django.views.decorators.DeadlockError`` will be raised, which can be
308+propagated to the user or handled in a middleware.
309+
310+Currently the ``postgresql_psycopg2``, ``oracle``, and ``mysql``
311+database backends support ``select_for_update()`` but MySQL has no
312+support for the ``nowait`` argument. Other backends will simply
313+generate queries as if ``select_for_update()`` had not been used.
314+
315 QuerySet methods that do not return QuerySets
316 ---------------------------------------------
317 
318Index: docs/ref/databases.txt
319===================================================================
320--- docs/ref/databases.txt      (wersja 9962)
321+++ docs/ref/databases.txt      (kopia robocza)
322@@ -276,6 +276,15 @@
323 column types have a maximum length restriction of 255 characters, regardless
324 of whether ``unique=True`` is specified or not.
325 
326+Row locking with ``QuerySet.select_for_update()``
327+-------------------------------------------------
328+
329+MySQL does not support the NOWAIT option to the SELECT ... FOR UPDATE
330+statement. However, you may call the ``select_for_update()`` method of a
331+queryset with ``nowait=True``. In that case, the argument will be silently
332+discarded and the generated query will block until the requested lock can be
333+acquired.
334+
335 .. _sqlite-notes:
336 
337 SQLite notes