Skip to content

Commit cc8ce89

Browse files
committed
feat(core): Use strict regex to identify lock contenders.
1 parent 225eeec commit cc8ce89

File tree

2 files changed

+93
-70
lines changed

2 files changed

+93
-70
lines changed

kazoo/recipe/lock.py

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
and/or the lease has been lost.
1515
1616
"""
17+
import re
1718
import sys
1819

1920
try:
@@ -83,35 +84,38 @@ class Lock(object):
8384
# sequence number. Involved in read/write locks.
8485
_EXCLUDE_NAMES = ["__lock__"]
8586

86-
def __init__(
87-
self, client, path, identifier=None, additional_lock_patterns=()
88-
):
87+
def __init__(self, client, path, identifier=None, extra_lock_patterns=()):
8988
"""Create a Kazoo lock.
9089
9190
:param client: A :class:`~kazoo.client.KazooClient` instance.
9291
:param path: The lock path to use.
9392
:param identifier: Name to use for this lock contender. This can be
9493
useful for querying to see who the current lock
9594
contenders are.
96-
:param additional_lock_patterns: Strings that will be used to
97-
identify other znode in the path
98-
that should be considered contenders
99-
for this lock.
100-
Use this for cross-implementation
101-
compatibility.
95+
:param extra_lock_patterns: Strings that will be used to
96+
identify other znode in the path
97+
that should be considered contenders
98+
for this lock.
99+
Use this for cross-implementation
100+
compatibility.
102101
103102
.. versionadded:: 2.7.1
104-
The additional_lock_patterns option.
103+
The extra_lock_patterns option.
105104
"""
106105
self.client = client
107106
self.path = path
108107
self._exclude_names = set(
109-
self._EXCLUDE_NAMES + list(additional_lock_patterns)
108+
self._EXCLUDE_NAMES + list(extra_lock_patterns)
109+
)
110+
self._contenders_re = re.compile(
111+
r"(?:{patterns})(-?\d{{10}})$".format(
112+
patterns="|".join(self._exclude_names)
113+
)
110114
)
111115

112116
# some data is written to the node. this can be queried via
113117
# contenders() to see who is contending for the lock
114-
self.data = str(identifier or "").encode('utf-8')
118+
self.data = str(identifier or "").encode("utf-8")
115119
self.node = None
116120

117121
self.wake_event = client.handler.event_object()
@@ -186,6 +190,7 @@ def _acquire_lock():
186190
return False
187191
if not locked:
188192
# Lock acquire doesn't take a timeout, so simulate it...
193+
# XXX: This is not true in Py3 >= 3.2
189194
try:
190195
locked = retry(_acquire_lock)
191196
except RetryFailedError:
@@ -255,18 +260,8 @@ def _inner_acquire(self, blocking, timeout, ephemeral=True):
255260
if self.cancelled:
256261
raise CancelledError()
257262

258-
children = self._get_sorted_children()
259-
260-
try:
261-
our_index = children.index(node)
262-
except ValueError: # pragma: nocover
263-
# somehow we aren't in the children -- probably we are
264-
# recovering from a session failure and our ephemeral
265-
# node was removed
266-
raise ForceRetryError()
267-
268-
predecessor = self.predecessor(children, our_index)
269-
if not predecessor:
263+
predecessor = self._get_predecessor(node)
264+
if predecessor is None:
270265
return True
271266

272267
if not blocking:
@@ -289,36 +284,44 @@ def _inner_acquire(self, blocking, timeout, ephemeral=True):
289284
finally:
290285
self.client.remove_listener(self._watch_session)
291286

292-
def predecessor(self, children, index):
293-
for c in reversed(children[:index]):
294-
if any(n in c for n in self._exclude_names):
295-
return c
296-
return None
297-
298287
def _watch_predecessor(self, event):
299288
self.wake_event.set()
300289

301-
def _get_sorted_children(self):
290+
def _get_predecessor(self, node):
291+
"""returns `node`'s predecessor or None
292+
293+
Note: This handle the case where the current lock is not a contender
294+
(e.g. rlock), this and also edge cases where the lock's ephemeral node
295+
is gone.
296+
"""
302297
children = self.client.get_children(self.path)
298+
found_self = False
299+
# Filter out the contenders using the computed regex
300+
contender_matches = []
301+
for child in children:
302+
match = self._contenders_re.search(child)
303+
if match is not None:
304+
contender_matches.append(match)
305+
if child == node:
306+
# Remember the node's match object so we can short circuit
307+
# below.
308+
found_self = match
309+
310+
if found_self is False: # pragma: nocover
311+
# somehow we aren't in the childrens -- probably we are
312+
# recovering from a session failure and our ephemeral
313+
# node was removed.
314+
raise ForceRetryError()
315+
316+
predecessor = None
317+
# Sort the contenders using the sequence number extracted by the regex,
318+
# then extract the original string.
319+
for match in sorted(contender_matches, key=lambda m: m.groups()):
320+
if match is found_self:
321+
break
322+
predecessor = match.string
303323

304-
# Node names are prefixed by a type: strip the prefix first, which may
305-
# be one of multiple values in case of a read-write lock, and return
306-
# only the sequence number (as a string since it is padded and will
307-
# sort correctly anyway).
308-
#
309-
# In some cases, the lock path may contain nodes with other prefixes
310-
# (eg. in case of a lease), just sort them last ('~' sorts after all
311-
# ASCII digits).
312-
def _seq(c):
313-
for name in self._exclude_names:
314-
idx = c.find(name)
315-
if idx != -1:
316-
return c[idx + len(name):]
317-
# Sort unknown node names eg. "lease_holder" last.
318-
return '~'
319-
320-
children.sort(key=_seq)
321-
return children
324+
return predecessor
322325

323326
def _find_node(self):
324327
children = self.client.get_children(self.path)
@@ -369,16 +372,37 @@ def contenders(self):
369372
if not self.assured_path:
370373
self._ensure_path()
371374

372-
children = self._get_sorted_children()
373-
374-
contenders = []
375+
children = self.client.get_children(self.path)
376+
# We want all contenders, including self (this is especially important
377+
# for r/w locks). This is similar to the logic of `_get_predecessor`
378+
# except we include our own pattern.
379+
all_contenders_re = re.compile(
380+
r"(?:{patterns})(-?\d{{10}})$".format(
381+
patterns="|".join(self._exclude_names | {self._NODE_NAME})
382+
)
383+
)
384+
# Filter out the contenders using the computed regex
385+
contender_matches = []
375386
for child in children:
387+
match = all_contenders_re.search(child)
388+
if match is not None:
389+
contender_matches.append(match)
390+
# Sort the contenders using the sequence number extracted by the regex,
391+
# then extract the original string.
392+
contender_nodes = [
393+
match.string
394+
for match in sorted(contender_matches, key=lambda m: m.groups())
395+
]
396+
# Retrieve all the contender nodes data (preserving order).
397+
contenders = []
398+
for node in contender_nodes:
376399
try:
377-
data, stat = self.client.get(self.path + "/" + child)
400+
data, stat = self.client.get(self.path + "/" + node)
378401
if data is not None:
379-
contenders.append(data.decode('utf-8'))
402+
contenders.append(data.decode("utf-8"))
380403
except NoNodeError: # pragma: nocover
381404
pass
405+
382406
return contenders
383407

384408
def __enter__(self):
@@ -508,12 +532,12 @@ def __init__(self, client, path, identifier=None, max_leases=1):
508532

509533
# some data is written to the node. this can be queried via
510534
# contenders() to see who is contending for the lock
511-
self.data = str(identifier or "").encode('utf-8')
535+
self.data = str(identifier or "").encode("utf-8")
512536
self.max_leases = max_leases
513537
self.wake_event = client.handler.event_object()
514538

515539
self.create_path = self.path + "/" + uuid.uuid4().hex
516-
self.lock_path = path + '-' + '__lock__'
540+
self.lock_path = path + "-" + "__lock__"
517541
self.is_acquired = False
518542
self.assured_path = False
519543
self.cancelled = False
@@ -526,7 +550,7 @@ def _ensure_path(self):
526550
# node did already exist
527551
data, _ = self.client.get(self.path)
528552
try:
529-
leases = int(data.decode('utf-8'))
553+
leases = int(data.decode("utf-8"))
530554
except (ValueError, TypeError):
531555
# ignore non-numeric data, maybe the node data is used
532556
# for other purposes
@@ -538,7 +562,7 @@ def _ensure_path(self):
538562
% (leases, self.max_leases)
539563
)
540564
else:
541-
self.client.set(self.path, str(self.max_leases).encode('utf-8'))
565+
self.client.set(self.path, str(self.max_leases).encode("utf-8"))
542566

543567
def cancel(self):
544568
"""Cancel a pending semaphore acquire."""
@@ -702,7 +726,7 @@ def lease_holders(self):
702726
for child in children:
703727
try:
704728
data, stat = self.client.get(self.path + "/" + child)
705-
lease_holders.append(data.decode('utf-8'))
729+
lease_holders.append(data.decode("utf-8"))
706730
except NoNodeError: # pragma: nocover
707731
pass
708732
return lease_holders

kazoo/tests/test_lock.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ def test_read_lock(self):
434434
# and that it's still not reentrant.
435435
gotten = lock.acquire(blocking=False)
436436
assert gotten is False
437-
438437
# Test that a second client we can share the same read lock
439438
client2 = self._get_client()
440439
client2.start()
@@ -444,7 +443,6 @@ def test_read_lock(self):
444443
assert lock2.is_acquired is True
445444
gotten = lock2.acquire(blocking=False)
446445
assert gotten is False
447-
448446
# Test that a writer is unable to share it
449447
client3 = self._get_client()
450448
client3.start()
@@ -741,24 +739,25 @@ def _thread(sem, event, timeout):
741739

742740

743741
class TestSequence(unittest.TestCase):
744-
def test_get_sorted_children(self):
742+
def test_get_predecessor(self):
743+
"""Validate selection of predecessors.
744+
"""
745745
goLock = "_c_8eb60557ba51e0da67eefc47467d3f34-lock-0000000031"
746746
pyLock = "514e5a831836450cb1a56c741e990fd8__lock__0000000032"
747747
children = ["hello", goLock, "world", pyLock]
748748
client = mock.MagicMock()
749749
client.get_children.return_value = children
750750
lock = Lock(client, "test")
751-
sorted_children = lock._get_sorted_children()
752-
assert len(sorted_children) == 4
753-
assert sorted_children[0] == pyLock
751+
assert lock._get_predecessor(pyLock) is None
754752

755-
def test_get_sorted_children_go(self):
753+
def test_get_predecessor_go(self):
754+
"""Test selection of predecessor when instructed to consider go-zk
755+
locks.
756+
"""
756757
goLock = "_c_8eb60557ba51e0da67eefc47467d3f34-lock-0000000031"
757758
pyLock = "514e5a831836450cb1a56c741e990fd8__lock__0000000032"
758759
children = ["hello", goLock, "world", pyLock]
759760
client = mock.MagicMock()
760761
client.get_children.return_value = children
761-
lock = Lock(client, "test", additional_lock_patterns=["-lock-"])
762-
sorted_children = lock._get_sorted_children()
763-
assert len(sorted_children) == 4
764-
assert sorted_children[0] == goLock
762+
lock = Lock(client, "test", extra_lock_patterns=["-lock-"])
763+
assert lock._get_predecessor(pyLock) == goLock

0 commit comments

Comments
 (0)