14
14
and/or the lease has been lost.
15
15
16
16
"""
17
+ import re
17
18
import sys
19
+
18
20
try :
19
21
from time import monotonic as now
20
22
except ImportError :
27
29
CancelledError ,
28
30
KazooException ,
29
31
LockTimeout ,
30
- NoNodeError
32
+ NoNodeError ,
31
33
)
32
34
from kazoo .protocol .states import KazooState
33
35
from kazoo .retry import (
34
36
ForceRetryError ,
35
37
KazooRetry ,
36
- RetryFailedError
38
+ RetryFailedError ,
37
39
)
38
40
39
41
@@ -82,22 +84,38 @@ class Lock(object):
82
84
# sequence number. Involved in read/write locks.
83
85
_EXCLUDE_NAMES = ["__lock__" ]
84
86
85
- def __init__ (self , client , path , identifier = None ):
87
+ def __init__ (self , client , path , identifier = None , extra_lock_patterns = () ):
86
88
"""Create a Kazoo lock.
87
89
88
90
:param client: A :class:`~kazoo.client.KazooClient` instance.
89
91
:param path: The lock path to use.
90
- :param identifier: Name to use for this lock contender. This
91
- can be useful for querying to see who the
92
- current lock contenders are.
93
-
92
+ :param identifier: Name to use for this lock contender. This can be
93
+ useful for querying to see who the current lock
94
+ contenders are.
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.
101
+
102
+ .. versionadded:: 2.7.1
103
+ The extra_lock_patterns option.
94
104
"""
95
105
self .client = client
96
106
self .path = path
107
+ self ._exclude_names = set (
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
+ )
114
+ )
97
115
98
116
# some data is written to the node. this can be queried via
99
117
# contenders() to see who is contending for the lock
100
- self .data = str (identifier or "" ).encode (' utf-8' )
118
+ self .data = str (identifier or "" ).encode (" utf-8" )
101
119
self .node = None
102
120
103
121
self .wake_event = client .handler .event_object ()
@@ -113,8 +131,9 @@ def __init__(self, client, path, identifier=None):
113
131
self .is_acquired = False
114
132
self .assured_path = False
115
133
self .cancelled = False
116
- self ._retry = KazooRetry (max_tries = None ,
117
- sleep_func = client .handler .sleep_func )
134
+ self ._retry = KazooRetry (
135
+ max_tries = None , sleep_func = client .handler .sleep_func
136
+ )
118
137
self ._lock = client .handler .lock_object ()
119
138
120
139
def _ensure_path (self ):
@@ -171,6 +190,7 @@ def _acquire_lock():
171
190
return False
172
191
if not locked :
173
192
# Lock acquire doesn't take a timeout, so simulate it...
193
+ # XXX: This is not true in Py3 >= 3.2
174
194
try :
175
195
locked = retry (_acquire_lock )
176
196
except RetryFailedError :
@@ -179,9 +199,12 @@ def _acquire_lock():
179
199
try :
180
200
gotten = False
181
201
try :
182
- gotten = retry (self ._inner_acquire ,
183
- blocking = blocking , timeout = timeout ,
184
- ephemeral = ephemeral )
202
+ gotten = retry (
203
+ self ._inner_acquire ,
204
+ blocking = blocking ,
205
+ timeout = timeout ,
206
+ ephemeral = ephemeral ,
207
+ )
185
208
except RetryFailedError :
186
209
pass
187
210
except KazooException :
@@ -222,8 +245,9 @@ def _inner_acquire(self, blocking, timeout, ephemeral=True):
222
245
self .create_tried = True
223
246
224
247
if not node :
225
- node = self .client .create (self .create_path , self .data ,
226
- ephemeral = ephemeral , sequence = True )
248
+ node = self .client .create (
249
+ self .create_path , self .data , ephemeral = ephemeral , sequence = True
250
+ )
227
251
# strip off path to node
228
252
node = node [len (self .path ) + 1 :]
229
253
@@ -236,18 +260,8 @@ def _inner_acquire(self, blocking, timeout, ephemeral=True):
236
260
if self .cancelled :
237
261
raise CancelledError ()
238
262
239
- children = self ._get_sorted_children ()
240
-
241
- try :
242
- our_index = children .index (node )
243
- except ValueError : # pragma: nocover
244
- # somehow we aren't in the children -- probably we are
245
- # recovering from a session failure and our ephemeral
246
- # node was removed
247
- raise ForceRetryError ()
248
-
249
- predecessor = self .predecessor (children , our_index )
250
- if not predecessor :
263
+ predecessor = self ._get_predecessor (node )
264
+ if predecessor is None :
251
265
return True
252
266
253
267
if not blocking :
@@ -263,40 +277,51 @@ def _inner_acquire(self, blocking, timeout, ephemeral=True):
263
277
else :
264
278
self .wake_event .wait (timeout )
265
279
if not self .wake_event .isSet ():
266
- raise LockTimeout ("Failed to acquire lock on %s after "
267
- "%s seconds" % (self .path , timeout ))
280
+ raise LockTimeout (
281
+ "Failed to acquire lock on %s after %s seconds"
282
+ % (self .path , timeout )
283
+ )
268
284
finally :
269
285
self .client .remove_listener (self ._watch_session )
270
286
271
- def predecessor (self , children , index ):
272
- for c in reversed (children [:index ]):
273
- if any (n in c for n in self ._EXCLUDE_NAMES ):
274
- return c
275
- return None
276
-
277
287
def _watch_predecessor (self , event ):
278
288
self .wake_event .set ()
279
289
280
- 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
+ """
281
297
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
282
323
283
- # Node names are prefixed by a type: strip the prefix first, which may
284
- # be one of multiple values in case of a read-write lock, and return
285
- # only the sequence number (as a string since it is padded and will
286
- # sort correctly anyway).
287
- #
288
- # In some cases, the lock path may contain nodes with other prefixes
289
- # (eg. in case of a lease), just sort them last ('~' sorts after all
290
- # ASCII digits).
291
- def _seq (c ):
292
- for name in ["__lock__" , "__rlock__" ]:
293
- idx = c .find (name )
294
- if idx != - 1 :
295
- return c [idx + len (name ):]
296
- # Sort unknown node names eg. "lease_holder" last.
297
- return '~'
298
- children .sort (key = _seq )
299
- return children
324
+ return predecessor
300
325
301
326
def _find_node (self ):
302
327
children = self .client .get_children (self .path )
@@ -347,16 +372,37 @@ def contenders(self):
347
372
if not self .assured_path :
348
373
self ._ensure_path ()
349
374
350
- children = self ._get_sorted_children ()
351
-
352
- 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 = []
353
386
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 :
354
399
try :
355
- data , stat = self .client .get (self .path + "/" + child )
400
+ data , stat = self .client .get (self .path + "/" + node )
356
401
if data is not None :
357
- contenders .append (data .decode (' utf-8' ))
402
+ contenders .append (data .decode (" utf-8" ))
358
403
except NoNodeError : # pragma: nocover
359
404
pass
405
+
360
406
return contenders
361
407
362
408
def __enter__ (self ):
@@ -391,6 +437,7 @@ class WriteLock(Lock):
391
437
shared lock.
392
438
393
439
"""
440
+
394
441
_NODE_NAME = "__lock__"
395
442
_EXCLUDE_NAMES = ["__lock__" , "__rlock__" ]
396
443
@@ -420,6 +467,7 @@ class ReadLock(Lock):
420
467
shared lock.
421
468
422
469
"""
470
+
423
471
_NODE_NAME = "__rlock__"
424
472
_EXCLUDE_NAMES = ["__lock__" ]
425
473
@@ -458,6 +506,7 @@ class Semaphore(object):
458
506
The max_leases check.
459
507
460
508
"""
509
+
461
510
def __init__ (self , client , path , identifier = None , max_leases = 1 ):
462
511
"""Create a Kazoo Lock
463
512
@@ -483,12 +532,12 @@ def __init__(self, client, path, identifier=None, max_leases=1):
483
532
484
533
# some data is written to the node. this can be queried via
485
534
# contenders() to see who is contending for the lock
486
- self .data = str (identifier or "" ).encode (' utf-8' )
535
+ self .data = str (identifier or "" ).encode (" utf-8" )
487
536
self .max_leases = max_leases
488
537
self .wake_event = client .handler .event_object ()
489
538
490
539
self .create_path = self .path + "/" + uuid .uuid4 ().hex
491
- self .lock_path = path + '-' + ' __lock__'
540
+ self .lock_path = path + "-" + " __lock__"
492
541
self .is_acquired = False
493
542
self .assured_path = False
494
543
self .cancelled = False
@@ -501,19 +550,19 @@ def _ensure_path(self):
501
550
# node did already exist
502
551
data , _ = self .client .get (self .path )
503
552
try :
504
- leases = int (data .decode (' utf-8' ))
553
+ leases = int (data .decode (" utf-8" ))
505
554
except (ValueError , TypeError ):
506
555
# ignore non-numeric data, maybe the node data is used
507
556
# for other purposes
508
557
pass
509
558
else :
510
559
if leases != self .max_leases :
511
560
raise ValueError (
512
- "Inconsistent max leases: %s, expected: %s" %
513
- (leases , self .max_leases )
561
+ "Inconsistent max leases: %s, expected: %s"
562
+ % (leases , self .max_leases )
514
563
)
515
564
else :
516
- 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" ))
517
566
518
567
def cancel (self ):
519
568
"""Cancel a pending semaphore acquire."""
@@ -548,7 +597,8 @@ def acquire(self, blocking=True, timeout=None):
548
597
549
598
try :
550
599
self .is_acquired = self .client .retry (
551
- self ._inner_acquire , blocking = blocking , timeout = timeout )
600
+ self ._inner_acquire , blocking = blocking , timeout = timeout
601
+ )
552
602
except KazooException :
553
603
# if we did ultimately fail, attempt to clean up
554
604
self ._best_effort_cleanup ()
@@ -590,8 +640,9 @@ def _inner_acquire(self, blocking, timeout=None):
590
640
self .wake_event .wait (w .leftover ())
591
641
if not self .wake_event .isSet ():
592
642
raise LockTimeout (
593
- "Failed to acquire semaphore on %s "
594
- "after %s seconds" % (self .path , timeout ))
643
+ "Failed to acquire semaphore on %s"
644
+ " after %s seconds" % (self .path , timeout )
645
+ )
595
646
else :
596
647
return False
597
648
finally :
@@ -612,8 +663,9 @@ def _get_lease(self, data=None):
612
663
# Get a list of the current potential lock holders. If they change,
613
664
# notify our wake_event object. This is used to unblock a blocking
614
665
# self._inner_acquire call.
615
- children = self .client .get_children (self .path ,
616
- self ._watch_lease_change )
666
+ children = self .client .get_children (
667
+ self .path , self ._watch_lease_change
668
+ )
617
669
618
670
# If there are leases available, acquire one
619
671
if len (children ) < self .max_leases :
@@ -674,7 +726,7 @@ def lease_holders(self):
674
726
for child in children :
675
727
try :
676
728
data , stat = self .client .get (self .path + "/" + child )
677
- lease_holders .append (data .decode (' utf-8' ))
729
+ lease_holders .append (data .decode (" utf-8" ))
678
730
except NoNodeError : # pragma: nocover
679
731
pass
680
732
return lease_holders
0 commit comments