Code

Ticket #2705: for_update_1.2.0-final.patch.diff

File for_update_1.2.0-final.patch.diff, 12.2 KB (added by ftmo, 4 years ago)

based on the file for_update_11366_cdestigter.diff

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