Skip to content

Commit f18d043

Browse files
committed
WL11282: Support new locking modes NOWAIT and SKIP LOCKED
This WL adds NOWAIT and SKIP_LOCKED support to ReadStatement.lock_shared() and ReadStatement.lock_exclusive() introduced in MySQL 8.0. A new enumerator was added to specify the lock contention to be used in the lock methods. Tests were added for regression.
1 parent 988e50b commit f18d043

File tree

7 files changed

+340
-9
lines changed

7 files changed

+340
-9
lines changed

docs/mysqlx/tutorials.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Tutorials
77
tutorials/getting_started
88
tutorials/transactions
99
tutorials/creating_indexes
10+
tutorials/locking

docs/mysqlx/tutorials/locking.rst

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
Locking
2+
=======
3+
4+
Shared and Exclusive Locks
5+
--------------------------
6+
7+
The X DevAPI supports locking matching rows, for the :func:`mysqlx.Collection.find()` and :func:`mysqlx.Table.select()` methods, which allows safe and transactional document/row updates on collections or tables.
8+
9+
There are two types of locks:
10+
11+
- :func:`mysqlx.ReadStatement.lock_shared()` permits the transaction that holds the lock to read a row.
12+
13+
- :func:`mysqlx.ReadStatement.lock_exclusive()` permits the transaction that holds the lock to update or delete a row.
14+
15+
Examples
16+
^^^^^^^^
17+
18+
**Setup**
19+
20+
Assuming the existence of ``test_schema.test_collection`` collection.
21+
22+
.. code-block:: python
23+
24+
[{
25+
"_id": "1",
26+
"name": "Fred",
27+
"age": 21
28+
},{
29+
"_id": "2",
30+
"name": "Sakila",
31+
"age": 23
32+
},{
33+
"_id": "3",
34+
"name": "Mike",
35+
"age": 42
36+
}]
37+
38+
Get the session and collection objects.
39+
40+
.. code-block:: python
41+
42+
# client 1
43+
session_1 = mysqlx.get_session("root:@localhost:33060")
44+
schema_1 = session_1.get_schema("test_schema")
45+
collection_1 = schema_1.get_collection("test_collection")
46+
47+
# client 2
48+
session_2 = mysqlx.get_session("root:@localhost:33060")
49+
schema_2 = session_2.get_schema("test_schema")
50+
collection_2 = schema_2.get_collection("test_collection")
51+
52+
**Shared lock**
53+
54+
.. code-block:: python
55+
56+
57+
# client 1
58+
session_1.start_transaction()
59+
collection_1.find("_id = '1'").lock_shared().execute()
60+
61+
# client 2
62+
session_2.start_transaction()
63+
collection_2.find("_id = '2'").lock_shared().execute() # should return immediately
64+
collection_2.find("_id = '1'").lock_shared().execute() # should return immediately
65+
66+
# client 1
67+
session_1.rollback()
68+
69+
# client 2
70+
session_2.rollback()
71+
72+
**Exclusive Lock**
73+
74+
.. code-block:: python
75+
76+
# client 1
77+
session_1.start_transaction()
78+
collection_1.find("_id = '1'").lock_exclusive().execute()
79+
80+
# client 2
81+
session_2.start_transaction()
82+
collection_2.find("_id = '2'").lock_exclusive().execute() # should return immediately
83+
collection_2.find("_id = '1'").lock_exclusive().execute() # session_2 should block
84+
85+
# client 1
86+
session_1.rollback() # session_2 should unblock now
87+
88+
# client 2
89+
session_2.rollback()
90+
91+
**Shared Lock after Exclusive**
92+
93+
.. code-block:: python
94+
95+
# client 1
96+
session_1.start_transaction()
97+
collection_1.find("_id = '1'").lock_exclusive().execute()
98+
99+
# client 2
100+
session_2.start_transaction()
101+
collection_2.find("_id = '2'").lock_shared().execute() # should return immediately
102+
collection_2.find("_id = '1'").lock_shared().execute() # session_2 blocks
103+
104+
# client 1
105+
session_1.rollback() # session_2 should unblock now
106+
107+
# client 2
108+
session_2.rollback()
109+
110+
**Exclusive Lock after Shared**
111+
112+
.. code-block:: python
113+
114+
# client 1
115+
session_1.start_transaction()
116+
collection_1.find("_id in ('1', '3')").lock_shared().execute()
117+
118+
# client 2
119+
session_2.start_transaction()
120+
collection_2.find("_id = '2'").lock_exclusive().execute() # should return immediately
121+
collection_2.find("_id = '3'").lock_shared().execute() # should return immediately
122+
collection_2.find("_id = '1'").lock_exclusive().execute() # session_2 should block
123+
124+
# client 1
125+
session_1.rollback() # session_2 should unblock now
126+
127+
# client 2
128+
session_2.rollback()
129+
130+
Locking with NOWAIT and SKIP_LOCKED
131+
-----------------------------------
132+
133+
If a row is locked by a transaction, a transaction that requests the same locked row must wait until the blocking transaction releases the row lock. However, waiting for a row lock to be released is not necessary if you want the query to return immediately when a requested row is locked, or if excluding locked rows from the result set is acceptable.
134+
135+
To avoid waiting for other transactions to release row locks, ``mysqlx.LockContention.NOWAIT`` and ``mysqlx.LockContention.SKIP_LOCKED`` lock contentions options may be used.
136+
137+
**NOWAIT**
138+
139+
A locking read that uses ``mysqlx.LockContention.NOWAIT`` never waits to acquire a row lock. The query executes immediately, failing with an error if a requested row is locked.
140+
141+
Example of reading a share locked document using :func:`mysqlx.ReadStatement.lock_shared()`:
142+
143+
.. code-block:: python
144+
145+
# client 1
146+
session_1.start_transaction()
147+
collection_1.find("_id = :id").lock_shared().bind("id", "1").execute()
148+
149+
# client 2
150+
session_2.start_transaction()
151+
collection_2.find("_id = :id").lock_shared(mysqlx.LockContention.NOWAIT) \
152+
.bind("id", "1").execute()
153+
# The execution should return immediately, no block and no error is thrown
154+
155+
collection_2.modify("_id = '1'").set("age", 43).execute()
156+
# The transaction should be blocked
157+
158+
# client 1
159+
session_1.commit()
160+
# session_2 should unblock now
161+
162+
# client 2
163+
session_2.rollback()
164+
165+
**SKIP_LOCKED**
166+
167+
A locking read that uses ``mysqlx.LockContention.SKIP_LOCKED`` never waits to acquire a row lock. The query executes immediately, removing locked rows from the result set.
168+
169+
Example of reading a share locked document using :func:`mysqlx.ReadStatement.lock_exclusive()`:
170+
171+
.. code-block:: python
172+
173+
# client 1
174+
session_1.start_transaction()
175+
collection_1.find("_id = :id").lock_shared().bind("id", "1").execute()
176+
177+
# client 2
178+
session_2.start_transaction()
179+
collection_2.find("_id = :id").lock_exclusive(mysqlx.LockContention.SKIP_LOCKED) \
180+
.bind("id", "1").execute()
181+
# The execution should return immediately, no error is thrown
182+
183+
# client 1
184+
session_1.commit()
185+
186+
# client 2
187+
collection_2.find("_id = :id").lock_exclusive(mysqlx.LockContention.SKIP_LOCKED) \
188+
.bind("id", 1).execute()
189+
# Since commit is done in 'client 1' then the read must be possible now and
190+
# no error is thrown
191+
session_2.rollback()
192+

lib/mysqlx/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
from .compat import STRING_TYPES, urlparse, unquote, parse_qsl
3535
from .connection import Session
36-
from .constants import SSLMode, Auth
36+
from .constants import Auth, LockContention, SSLMode
3737
from .crud import Schema, Collection, Table, View
3838
from .dbdoc import DbDoc
3939
from .errors import (Error, InterfaceError, DatabaseError, NotSupportedError,
@@ -269,7 +269,7 @@ def get_session(*args, **kwargs):
269269
"Session", "get_session",
270270

271271
# mysqlx.constants
272-
"constants",
272+
"Auth", "LockContention", "SSLMode",
273273

274274
# mysqlx.crud
275275
"Schema", "Collection", "Table", "View",

lib/mysqlx/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -55,5 +55,7 @@ def create_enum(name, fields, values=None):
5555
Auth = create_enum("Auth",
5656
("PLAIN", "EXTERNAL", "MYSQL41"),
5757
("plain", "external", "mysql41"))
58+
LockContention = create_enum("LockContention",
59+
("DEFAULT", "NOWAIT", "SKIP_LOCKED"), (0, 1, 2))
5860

59-
__all__ = ["SSLMode", "Auth"]
61+
__all__ = ["SSLMode", "Auth", "LockContention"]

lib/mysqlx/protocol.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ def send_find(self, stmt):
397397
msg["locking"] = \
398398
mysqlxpb_enum("Mysqlx.Crud.Find.RowLock.SHARED_LOCK")
399399

400+
if stmt.lock_contention > 0:
401+
msg["locking_options"] = stmt.lock_contention
402+
400403
self._writer.write_message(
401404
mysqlxpb_enum("Mysqlx.ClientMessages.Type.CRUD_FIND"), msg)
402405

lib/mysqlx/statement.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .errors import ProgrammingError, NotSupportedError
3535
from .expr import ExprParser
3636
from .compat import STRING_TYPES
37+
from .constants import LockContention
3738
from .dbdoc import DbDoc
3839
from .result import SqlResult, Result
3940
from .protobuf import mysqlxpb_enum
@@ -709,6 +710,29 @@ def __init__(self, target, doc_based=True, condition=None):
709710
super(ReadStatement, self).__init__(target, doc_based, condition)
710711
self._lock_exclusive = False
711712
self._lock_shared = False
713+
self._lock_contention = LockContention.DEFAULT
714+
715+
@property
716+
def lock_contention(self):
717+
""":class:`mysqlx.LockContention`: The lock contention value."""
718+
return self._lock_contention
719+
720+
def _set_lock_contention(self, lock_contention):
721+
"""Set the lock contention.
722+
723+
Args:
724+
lock_contention (:class:`mysqlx.LockContention`): Lock contention.
725+
726+
Raises:
727+
ProgrammingError: If is an invalid lock contention value.
728+
"""
729+
try:
730+
# Check if is a valid lock contention value
731+
_ = LockContention.index(lock_contention)
732+
except ValueError:
733+
raise ProgrammingError("Invalid lock contention mode. Use 'NOWAIT' "
734+
"or 'SKIP_LOCKED'")
735+
self._lock_contention = lock_contention
712736

713737
def is_lock_exclusive(self):
714738
"""Returns `True` if is `EXCLUSIVE LOCK`.
@@ -726,20 +750,28 @@ def is_lock_shared(self):
726750
"""
727751
return self._lock_shared
728752

729-
def lock_shared(self):
730-
"""Execute a read operation with SHARED LOCK. Only one lock can be
753+
def lock_shared(self, lock_contention=LockContention.DEFAULT):
754+
"""Execute a read operation with `SHARED LOCK`. Only one lock can be
731755
active at a time.
756+
757+
Args:
758+
lock_contention (:class:`mysqlx.LockContention`): Lock contention.
732759
"""
733760
self._lock_exclusive = False
734761
self._lock_shared = True
762+
self._set_lock_contention(lock_contention)
735763
return self
736764

737-
def lock_exclusive(self):
738-
"""Execute a read operation with EXCLUSIVE LOCK. Only one lock can be
765+
def lock_exclusive(self, lock_contention=LockContention.DEFAULT):
766+
"""Execute a read operation with `EXCLUSIVE LOCK`. Only one lock can be
739767
active at a time.
768+
769+
Args:
770+
lock_contention (:class:`mysqlx.LockContention`): Lock contention.
740771
"""
741772
self._lock_exclusive = True
742773
self._lock_shared = False
774+
self._set_lock_contention(lock_contention)
743775
return self
744776

745777
def group_by(self, *fields):
@@ -849,6 +881,7 @@ def get_sql(self):
849881
having=having, order=order_by))
850882
return stmt
851883

884+
852885
class InsertStatement(WriteStatement):
853886
"""A statement for insert operations on Table.
854887

0 commit comments

Comments
 (0)