zk 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.dotfiles/ruby-gemset +1 -0
- data/.dotfiles/ruby-version +1 -0
- data/.gitignore +2 -0
- data/.gitmodules +1 -1
- data/.travis.yml +1 -0
- data/README.markdown +11 -12
- data/RELEASES.markdown +6 -1
- data/lib/zk/client/base.rb +1 -1
- data/lib/zk/locker.rb +12 -0
- data/lib/zk/locker/exclusive_locker.rb +6 -48
- data/lib/zk/locker/locker_base.rb +80 -11
- data/lib/zk/locker/semaphore.rb +80 -0
- data/lib/zk/locker/shared_locker.rb +7 -78
- data/lib/zk/node_deletion_watcher.rb +86 -30
- data/lib/zk/version.rb +1 -1
- data/spec/shared/locker_contexts.rb +7 -1
- data/spec/zk/locker/semaphore_spec.rb +165 -0
- data/spec/zk/locker/shared_exclusive_integration_spec.rb +47 -0
- data/spec/zk/node_deletion_watcher_spec.rb +102 -1
- metadata +9 -4
@@ -0,0 +1 @@
|
|
1
|
+
zk
|
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p429
|
data/.gitignore
CHANGED
data/.gitmodules
CHANGED
data/.travis.yml
CHANGED
data/README.markdown
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ZK #
|
2
2
|
|
3
|
-
[](https://secure.travis-ci.org/zk-ruby/zk.png?branch=master)](http://travis-ci.org/zk-ruby/zk)
|
4
4
|
|
5
5
|
ZK is an application programmer's interface to the Apache [ZooKeeper][] server. It is based on the [zookeeper gem][] which is a multi-Ruby low-level driver. Currently MRI 1.8.7, 1.9.2, 1.9.3, REE, and JRuby are supported. Rubinius 2.0.testing is supported-ish (it's expected to work, but upstream is unstable, so YMMV).
|
6
6
|
|
@@ -16,10 +16,10 @@ Development is sponsored by [Snapfish][] and has been generously released to the
|
|
16
16
|
|
17
17
|
[ZK::Client::Base]: http://rubydoc.info/gems/zk/ZK/Client/Base
|
18
18
|
[ZooKeeper]: http://zookeeper.apache.org/ "Apache ZooKeeper"
|
19
|
-
[zookeeper gem]: https://github.com/
|
19
|
+
[zookeeper gem]: https://github.com/zk-ruby/zookeeper "zookeeper gem"
|
20
20
|
[MIT]: http://www.gnu.org/licenses/license-list.html#Expat "MIT (Expat) License"
|
21
21
|
[Snapfish]: http://www.snapfish.com/ "Snapfish"
|
22
|
-
[RELEASES]: https://github.com/
|
22
|
+
[RELEASES]: https://github.com/zk-ruby/zk/blob/master/RELEASES.markdown
|
23
23
|
|
24
24
|
## What is ZooKeeper? ##
|
25
25
|
|
@@ -38,7 +38,7 @@ ZooKeeper is easy to deploy in a [Highly Available][ha-config] configuration, an
|
|
38
38
|
[leader election]: http://zookeeper.apache.org/doc/current/recipes.html#sc_leaderElection
|
39
39
|
[group membership]: http://zookeeper.apache.org/doc/current/recipes.html#sc_outOfTheBox
|
40
40
|
[ha-config]: http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_CrossMachineRequirements "HA config"
|
41
|
-
[groups]: https://github.com/
|
41
|
+
[groups]: https://github.com/zk-ruby/zk-group
|
42
42
|
[locks]: http://rubydoc.info/gems/zk/ZK/Locker
|
43
43
|
|
44
44
|
|
@@ -62,7 +62,7 @@ In addition to all of that, I would like to think that the public API the ZK::Cl
|
|
62
62
|
[recipes]: http://zookeeper.apache.org/doc/current/recipes.html
|
63
63
|
[Mongoid]: http://mongoid.org/
|
64
64
|
[EventMachine]: https://github.com/eventmachine/eventmachine
|
65
|
-
[zk-eventmachine]: https://github.com/
|
65
|
+
[zk-eventmachine]: https://github.com/zk-ruby/zk-eventmachine
|
66
66
|
|
67
67
|
## Release info / Changelog
|
68
68
|
|
@@ -85,7 +85,7 @@ ZK strives to be a complete, correct, and convenient way of interacting with Zoo
|
|
85
85
|
* ZK::Client supports asynchronous calls of all basic methods (get, set, delete, etc.) however these versions are kind of inconvenient to use. For a fully evented stack, try [zk-eventmachine][], which is designed to be compatible and convenient to use in event-driven code.
|
86
86
|
|
87
87
|
[twitter/zookeeper]: https://github.com/twitter/zookeeper
|
88
|
-
[async-branch]: https://github.com/
|
88
|
+
[async-branch]: https://github.com/zk-ruby/zk/tree/dev%2Fasync-conveniences
|
89
89
|
[chroot]: http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#ch_zkSessions
|
90
90
|
[YARD]: http://yardoc.org/
|
91
91
|
[sessions]: http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#ch_zkSessions
|
@@ -100,19 +100,18 @@ ZK strives to be a complete, correct, and convenient way of interacting with Zoo
|
|
100
100
|
|
101
101
|
## Dependencies
|
102
102
|
|
103
|
-
* The [
|
103
|
+
* The [zookeeper gem][szk-gem] ([repo][szk-repo]).
|
104
104
|
|
105
105
|
* For JRuby, the [slyphon-zookeeper\_jar gem][szk-jar-gem] ([repo][szk-jar-repo]), which just wraps the upstream zookeeper driver jar in a gem for easy installation
|
106
106
|
|
107
|
-
[szk-gem]: https://rubygems.org/gems/
|
108
|
-
[szk-repo]: https://github.com/
|
109
|
-
[szk-repo-bundler]: https://github.com/
|
107
|
+
[szk-gem]: https://rubygems.org/gems/zookeeper
|
108
|
+
[szk-repo]: https://github.com/zk-ruby/zookeeper
|
109
|
+
[szk-repo-bundler]: https://github.com/zk-ruby/zookeeper/tree/dev/gemfile/
|
110
110
|
[szk-jar-gem]: https://rubygems.org/gems/slyphon-zookeeper_jar
|
111
|
-
[szk-jar-repo]: https://github.com/
|
111
|
+
[szk-jar-repo]: https://github.com/zk-ruby/zookeeper_jar
|
112
112
|
|
113
113
|
## Contacting the author
|
114
114
|
|
115
|
-
* I'm usually hanging out in IRC on freenode.net in the BRAND NEW #zk-gem channel.
|
116
115
|
* if you really want to, you can also reach me via twitter [@slyphon][]
|
117
116
|
|
118
117
|
[@slyphon]: https://twitter.com/#!/slyphon
|
data/RELEASES.markdown
CHANGED
data/lib/zk/client/base.rb
CHANGED
@@ -169,7 +169,7 @@ module ZK
|
|
169
169
|
# Create a node with the given path. The node data will be the given data.
|
170
170
|
# The path is returned.
|
171
171
|
#
|
172
|
-
# If the ephemeral option is given, the znode
|
172
|
+
# If the ephemeral option is given, the znode created will be removed by the
|
173
173
|
# server automatically when the session associated with the creation of the
|
174
174
|
# node expires. Note that ephemeral nodes cannot have children.
|
175
175
|
#
|
data/lib/zk/locker.rb
CHANGED
@@ -92,6 +92,7 @@ module ZK
|
|
92
92
|
module Locker
|
93
93
|
SHARED_LOCK_PREFIX = 'sh'.freeze
|
94
94
|
EXCLUSIVE_LOCK_PREFIX = 'ex'.freeze
|
95
|
+
SEMAPHORE_LOCK_PREFIX = 'sem'.freeze
|
95
96
|
|
96
97
|
@default_root_lock_node = '/_zklocking'.freeze unless @default_root_lock_node
|
97
98
|
|
@@ -118,6 +119,16 @@ module ZK
|
|
118
119
|
ExclusiveLocker.new(client, name, *args)
|
119
120
|
end
|
120
121
|
|
122
|
+
# Create a {Semaphore} instance
|
123
|
+
#
|
124
|
+
# @param client (see Semaphore#initialize)
|
125
|
+
# @param name (see Semaphore#initialize)
|
126
|
+
# @param semaphore_size (see Semaphore#initialize)
|
127
|
+
# @return [Semaphore]
|
128
|
+
def semaphore(client, name, semaphore_size, *args)
|
129
|
+
Semaphore.new(client, name, semaphore_size, *args)
|
130
|
+
end
|
131
|
+
|
121
132
|
# Clean up dead locker directories. There are situations (particularly
|
122
133
|
# session expiration) where a lock's directory will never be cleaned up.
|
123
134
|
#
|
@@ -164,4 +175,5 @@ require 'zk/locker/lock_options'
|
|
164
175
|
require 'zk/locker/locker_base'
|
165
176
|
require 'zk/locker/shared_locker'
|
166
177
|
require 'zk/locker/exclusive_locker'
|
178
|
+
require 'zk/locker/semaphore'
|
167
179
|
|
@@ -44,59 +44,17 @@ module ZK
|
|
44
44
|
end
|
45
45
|
|
46
46
|
private
|
47
|
-
def lock_with_opts_hash(opts)
|
48
|
-
create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@mutex.synchronize { @locked = true }
|
54
|
-
elsif lock_opts.blocking?
|
55
|
-
block_until_write_lock!(:timeout => lock_opts.timeout)
|
56
|
-
else
|
57
|
-
false
|
58
|
-
end
|
59
|
-
ensure
|
60
|
-
cleanup_lock_path! unless @mutex.synchronize { @locked }
|
61
|
-
end
|
62
|
-
|
63
|
-
# the node that is next-lowest in sequence number to ours, the one we
|
64
|
-
# watch for updates to
|
65
|
-
def next_lowest_node
|
66
|
-
ary = ordered_lock_children()
|
67
|
-
my_idx = ary.index(lock_basename)
|
68
|
-
|
69
|
-
raise WeAreTheLowestLockNumberException if my_idx == 0
|
70
|
-
|
71
|
-
ary[(my_idx - 1)]
|
48
|
+
# @private
|
49
|
+
def lock_prefix
|
50
|
+
EXCLUSIVE_LOCK_PREFIX
|
72
51
|
end
|
73
52
|
|
74
|
-
|
75
|
-
|
53
|
+
# @private
|
54
|
+
def blocking_locks
|
55
|
+
lower_lock_names
|
76
56
|
end
|
77
|
-
alias got_lock? got_write_lock?
|
78
57
|
|
79
|
-
def block_until_write_lock!(opts={})
|
80
|
-
begin
|
81
|
-
path = "#{root_lock_path}/#{next_lowest_node}"
|
82
|
-
logger.debug { "#{self.class}##{__method__} path=#{path.inspect}" }
|
83
|
-
|
84
|
-
@mutex.synchronize do
|
85
|
-
logger.debug { "assigning the @node_deletion_watcher" }
|
86
|
-
@node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
|
87
|
-
logger.debug { "broadcasting" }
|
88
|
-
@cond.broadcast
|
89
|
-
end
|
90
|
-
|
91
|
-
logger.debug { "calling block_until_deleted" }
|
92
|
-
Thread.pass
|
93
|
-
|
94
|
-
@node_deletion_watcher.block_until_deleted(opts)
|
95
|
-
rescue WeAreTheLowestLockNumberException
|
96
|
-
end
|
97
|
-
|
98
|
-
@mutex.synchronize { @locked = true }
|
99
|
-
end
|
100
58
|
end # ExclusiveLocker
|
101
59
|
end # Locker
|
102
60
|
end # ZK
|
@@ -102,6 +102,11 @@ module ZK
|
|
102
102
|
synchronize { lock_path and File.basename(lock_path) }
|
103
103
|
end
|
104
104
|
|
105
|
+
# @private
|
106
|
+
def lock_number
|
107
|
+
synchronize { lock_path and digit_from(lock_path) }
|
108
|
+
end
|
109
|
+
|
105
110
|
# returns our current idea of whether or not we hold the lock, which does
|
106
111
|
# not actually check the state on the server.
|
107
112
|
#
|
@@ -288,10 +293,6 @@ module ZK
|
|
288
293
|
end
|
289
294
|
|
290
295
|
private
|
291
|
-
def lock_with_opts_hash(opts={})
|
292
|
-
raise NotImplementedError
|
293
|
-
end
|
294
|
-
|
295
296
|
def synchronize
|
296
297
|
@mutex.synchronize { yield }
|
297
298
|
end
|
@@ -318,13 +319,6 @@ module ZK
|
|
318
319
|
retry
|
319
320
|
end
|
320
321
|
|
321
|
-
# performs the checks that (according to the recipe) mean that we hold
|
322
|
-
# the lock. used by (#assert!)
|
323
|
-
#
|
324
|
-
def got_lock?
|
325
|
-
raise NotImplementedError
|
326
|
-
end
|
327
|
-
|
328
322
|
# prefix is the string that will appear in front of the sequence num,
|
329
323
|
# defaults to 'lock'
|
330
324
|
#
|
@@ -395,6 +389,81 @@ module ZK
|
|
395
389
|
|
396
390
|
rval
|
397
391
|
end
|
392
|
+
|
393
|
+
# @private
|
394
|
+
def lower_lock_names(watch=false)
|
395
|
+
olc = ordered_lock_children(watch)
|
396
|
+
return olc unless lock_path
|
397
|
+
|
398
|
+
olc.select do |lock|
|
399
|
+
digit_from(lock) < lock_number
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# for write locks & semaphores, this will be all locks lower than us
|
404
|
+
# for read locks, this will be all write-locks lower than us.
|
405
|
+
# @return [Array] an array of string node paths
|
406
|
+
def blocking_locks
|
407
|
+
raise NotImplementedError
|
408
|
+
end
|
409
|
+
|
410
|
+
def lock_prefix
|
411
|
+
raise NotImplementedError
|
412
|
+
end
|
413
|
+
|
414
|
+
# performs the checks that (according to the recipe) mean that we hold
|
415
|
+
# the lock. used by (#assert!)
|
416
|
+
#
|
417
|
+
def got_lock?
|
418
|
+
lock_path and blocking_locks.empty?
|
419
|
+
end
|
420
|
+
|
421
|
+
# for write locks & read locks, this will be zero since #blocking_locks
|
422
|
+
# accounts for all locks that could block at all.
|
423
|
+
# for semaphores, this is one less than the semaphore size.
|
424
|
+
# @private
|
425
|
+
# @returns [Integer]
|
426
|
+
def allowed_blocking_locks_remaining
|
427
|
+
0
|
428
|
+
end
|
429
|
+
|
430
|
+
def blocking_locks_full_paths
|
431
|
+
blocking_locks.map { |partial| "#{root_lock_path}/#{partial}"}
|
432
|
+
end
|
433
|
+
|
434
|
+
def lock_with_opts_hash(opts)
|
435
|
+
create_lock_path!(lock_prefix)
|
436
|
+
|
437
|
+
lock_opts = LockOptions.new(opts)
|
438
|
+
|
439
|
+
if got_lock? or (lock_opts.blocking? and block_until_lock!(:timeout => lock_opts.timeout))
|
440
|
+
@mutex.synchronize { @locked = true }
|
441
|
+
else
|
442
|
+
false
|
443
|
+
end
|
444
|
+
ensure
|
445
|
+
cleanup_lock_path! unless @mutex.synchronize { @locked }
|
446
|
+
end
|
447
|
+
|
448
|
+
def block_until_lock!(opts={})
|
449
|
+
paths = blocking_locks_full_paths
|
450
|
+
|
451
|
+
logger.debug { "#{self.class}\##{__method__} paths=#{paths.inspect}" }
|
452
|
+
|
453
|
+
@mutex.synchronize do
|
454
|
+
logger.debug { "assigning the @node_deletion_watcher" }
|
455
|
+
ndw_options = {:threshold => allowed_blocking_locks_remaining}
|
456
|
+
@node_deletion_watcher = NodeDeletionWatcher.new(zk, paths, ndw_options)
|
457
|
+
logger.debug { "broadcasting" }
|
458
|
+
@cond.broadcast
|
459
|
+
end
|
460
|
+
|
461
|
+
logger.debug { "calling block_until_deleted" }
|
462
|
+
Thread.pass
|
463
|
+
|
464
|
+
@node_deletion_watcher.block_until_deleted(opts)
|
465
|
+
true
|
466
|
+
end
|
398
467
|
end # LockerBase
|
399
468
|
end # Locker
|
400
469
|
end # ZK
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module ZK
|
2
|
+
module Locker
|
3
|
+
# A semaphore implementation
|
4
|
+
class Semaphore < LockerBase
|
5
|
+
include Exceptions
|
6
|
+
|
7
|
+
@default_root_node = '/_zksemaphore'.freeze unless @default_root_node
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# the default root path we will use when a value is not given to a
|
11
|
+
# constructor
|
12
|
+
attr_accessor :default_root_node
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(client, name, semaphore_size, root_node=nil)
|
16
|
+
raise BadArgument, <<-EOMESSAGE unless semaphore_size.kind_of? Integer
|
17
|
+
semaphore_size must be Integer, not #{semaphore_size.inspect}
|
18
|
+
EOMESSAGE
|
19
|
+
|
20
|
+
@semaphore_size = semaphore_size
|
21
|
+
|
22
|
+
super(client, name, root_node || ::ZK::Locker::Semaphore.default_root_node)
|
23
|
+
end
|
24
|
+
|
25
|
+
# (see LockerBase#lock)
|
26
|
+
# obtain a shared lock.
|
27
|
+
#
|
28
|
+
def lock(opts={})
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
# (see LockerBase#assert!)
|
33
|
+
#
|
34
|
+
# checks that we:
|
35
|
+
#
|
36
|
+
# * we have obtained the lock (i.e. {#locked?} is true)
|
37
|
+
# * have a lock path
|
38
|
+
# * our lock path still exists
|
39
|
+
# * there are no exclusive locks with lower numbers than ours
|
40
|
+
#
|
41
|
+
def assert!
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
# (see LockerBase#acquirable?)
|
46
|
+
def acquirable?
|
47
|
+
return true if locked?
|
48
|
+
return false if blocked_by_semaphore?
|
49
|
+
true
|
50
|
+
rescue Exceptions::NoNode
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def blocked_by_semaphore?
|
55
|
+
( blocking_locks.size >= @semaphore_size )
|
56
|
+
end
|
57
|
+
|
58
|
+
# @private
|
59
|
+
def blocking_locks
|
60
|
+
lower_lock_names
|
61
|
+
end
|
62
|
+
|
63
|
+
# @private
|
64
|
+
def allowed_blocking_locks_remaining
|
65
|
+
@semaphore_size - 1
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
def lock_prefix
|
70
|
+
SEMAPHORE_LOCK_PREFIX
|
71
|
+
end
|
72
|
+
|
73
|
+
def got_semaphore?
|
74
|
+
synchronize { lock_path and not blocked_by_semaphore? }
|
75
|
+
end
|
76
|
+
alias_method :got_lock?, :got_semaphore?
|
77
|
+
|
78
|
+
end # Semaphore
|
79
|
+
end # Locker
|
80
|
+
end # ZK
|
@@ -23,98 +23,27 @@ module ZK
|
|
23
23
|
super
|
24
24
|
end
|
25
25
|
|
26
|
-
# (see LockerBase#locked?)
|
27
|
-
def locked?
|
28
|
-
false|@locked
|
29
|
-
end
|
30
|
-
|
31
26
|
# (see LockerBase#acquirable?)
|
32
27
|
def acquirable?
|
33
28
|
return true if locked?
|
34
|
-
|
29
|
+
blocking_locks.empty?
|
35
30
|
rescue Exceptions::NoNode
|
36
31
|
true
|
37
32
|
end
|
38
33
|
|
39
34
|
# @private
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
# returns the sequence number of the next lowest write lock node
|
45
|
-
#
|
46
|
-
# raises NoWriteLockFoundException when there are no write nodes with a
|
47
|
-
# sequence less than ours
|
48
|
-
#
|
49
|
-
# @private
|
50
|
-
def next_lowest_write_lock_num
|
51
|
-
digit_from(next_lowest_write_lock_name)
|
52
|
-
end
|
53
|
-
|
54
|
-
# the next lowest write lock number to ours
|
55
|
-
#
|
56
|
-
# so if we're "read010" and the children of the lock node are:
|
57
|
-
#
|
58
|
-
# %w[write008 write009 read010 read011]
|
59
|
-
#
|
60
|
-
# then this method will return write009
|
61
|
-
#
|
62
|
-
# raises NoWriteLockFoundException if there were no write nodes with an
|
63
|
-
# index lower than ours
|
64
|
-
#
|
65
|
-
# @private
|
66
|
-
def next_lowest_write_lock_name
|
67
|
-
ary = ordered_lock_children()
|
68
|
-
my_idx = ary.index(lock_basename) # our idx would be 2
|
69
|
-
|
70
|
-
ary[0..my_idx].reverse.find { |n| n.start_with?(EXCLUSIVE_LOCK_PREFIX) }.tap do |rv|
|
71
|
-
raise NoWriteLockFoundException if rv.nil?
|
35
|
+
def lower_write_lock_names
|
36
|
+
lower_lock_names.select do |lock|
|
37
|
+
lock.start_with?(EXCLUSIVE_LOCK_PREFIX)
|
72
38
|
end
|
73
39
|
end
|
40
|
+
alias :blocking_locks :lower_write_lock_names
|
74
41
|
|
75
42
|
# @private
|
76
|
-
def
|
77
|
-
|
78
|
-
rescue NoWriteLockFoundException
|
79
|
-
true
|
43
|
+
def lock_prefix
|
44
|
+
SHARED_LOCK_PREFIX
|
80
45
|
end
|
81
|
-
alias got_lock? got_read_lock?
|
82
|
-
|
83
|
-
private
|
84
|
-
def lock_with_opts_hash(opts)
|
85
|
-
create_lock_path!(SHARED_LOCK_PREFIX)
|
86
|
-
|
87
|
-
lock_opts = LockOptions.new(opts)
|
88
46
|
|
89
|
-
if got_read_lock?
|
90
|
-
@mutex.synchronize { @locked = true }
|
91
|
-
elsif lock_opts.blocking?
|
92
|
-
block_until_read_lock!(:timeout => lock_opts.timeout)
|
93
|
-
else
|
94
|
-
false
|
95
|
-
end
|
96
|
-
ensure
|
97
|
-
cleanup_lock_path! unless @mutex.synchronize { @locked }
|
98
|
-
end
|
99
|
-
|
100
|
-
def block_until_read_lock!(opts={})
|
101
|
-
begin
|
102
|
-
path = "#{root_lock_path}/#{next_lowest_write_lock_name}"
|
103
|
-
logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
|
104
|
-
|
105
|
-
@mutex.synchronize do
|
106
|
-
@node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
|
107
|
-
@cond.broadcast
|
108
|
-
end
|
109
|
-
|
110
|
-
@node_deletion_watcher.block_until_deleted(opts)
|
111
|
-
rescue NoWriteLockFoundException
|
112
|
-
# next_lowest_write_lock_name may raise NoWriteLockFoundException,
|
113
|
-
# which means we should not block as we have the lock (there is nothing to wait for)
|
114
|
-
end
|
115
|
-
|
116
|
-
@mutex.synchronize { @locked = true }
|
117
|
-
end
|
118
47
|
end # SharedLocker
|
119
48
|
end # Locker
|
120
49
|
end # ZK
|
@@ -14,11 +14,40 @@ module ZK
|
|
14
14
|
end
|
15
15
|
include Constants
|
16
16
|
|
17
|
-
attr_reader :zk,
|
17
|
+
attr_reader :zk,
|
18
|
+
:paths,
|
19
|
+
:options,
|
20
|
+
:watched_paths,
|
21
|
+
:remaining_paths,
|
22
|
+
:threshold
|
23
|
+
|
24
|
+
# Create a new NodeDeletionWatcher that has the ability to block until
|
25
|
+
# some or all of the paths given to it have been deleted.
|
26
|
+
#
|
27
|
+
# @param [ZK::client] zk
|
28
|
+
#
|
29
|
+
# @param [Array] paths - one or more paths to watch
|
30
|
+
#
|
31
|
+
# @param optional [Hash] options - Symbol-keyed hash
|
32
|
+
# @option options [Integer,false,nil] :threshold (0)
|
33
|
+
# the number of remaining nodes allowed when
|
34
|
+
# determining whether or not to continue blocking.
|
35
|
+
# If `false` or `nil` are provided, the default
|
36
|
+
# will be substituted.
|
37
|
+
#
|
38
|
+
def initialize( zk, paths, options={} )
|
39
|
+
paths = [paths] if paths.kind_of? String # old style single-node support
|
40
|
+
|
41
|
+
@zk = zk
|
42
|
+
@paths = paths.dup
|
43
|
+
@options = options.dup
|
44
|
+
@threshold = options[:threshold] || 0
|
45
|
+
raise BadArgument, <<-EOBADARG unless @threshold.kind_of? Integer
|
46
|
+
options[:threshold] must be an Integer. Got #{@threshold.inspect}."
|
47
|
+
EOBADARG
|
18
48
|
|
19
|
-
|
20
|
-
@
|
21
|
-
@path = path.dup
|
49
|
+
@watched_paths = []
|
50
|
+
@remaining_paths = paths.dup
|
22
51
|
|
23
52
|
@subs = []
|
24
53
|
|
@@ -92,25 +121,21 @@ module ZK
|
|
92
121
|
end
|
93
122
|
|
94
123
|
# @option opts [Numeric] :timeout (nil) if a positive integer, represents a duration in
|
95
|
-
# seconds after which, if
|
96
|
-
# be raised in all waiting threads
|
124
|
+
# seconds after which, if the threshold has not been met, a LockWaitTimeoutError will
|
125
|
+
# be raised in all waiting threads.
|
97
126
|
#
|
98
127
|
def block_until_deleted(opts={})
|
99
128
|
timeout = opts[:timeout]
|
100
129
|
|
101
130
|
@mutex.synchronize do
|
102
|
-
raise InvalidStateError, "Already fired for #{
|
131
|
+
raise InvalidStateError, "Already fired for #{status_string}" if @result
|
103
132
|
register_callbacks
|
104
133
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
@blocked = NOT_ANYMORE
|
109
|
-
@cond.broadcast # wake any waiting threads
|
110
|
-
return true
|
111
|
-
end
|
134
|
+
watch_appropriate_nodes
|
135
|
+
|
136
|
+
return finish_blocking if threshold_met?
|
112
137
|
|
113
|
-
logger.debug { "ok, going to block: #{
|
138
|
+
logger.debug { "ok, going to block: #{status_string}" }
|
114
139
|
|
115
140
|
@blocked = BLOCKED
|
116
141
|
@cond.broadcast # wake threads waiting for @blocked to change
|
@@ -119,14 +144,15 @@ module ZK
|
|
119
144
|
|
120
145
|
@blocked = NOT_ANYMORE
|
121
146
|
|
122
|
-
logger.debug { "got result
|
147
|
+
logger.debug { "got result: #{@result.inspect}. #{status_string}" }
|
123
148
|
|
124
149
|
case @result
|
125
150
|
when :deleted
|
126
|
-
logger.debug { "
|
151
|
+
logger.debug { "enough paths were deleted. #{status_string}" }
|
127
152
|
return true
|
128
153
|
when TIMED_OUT
|
129
|
-
raise ZK::Exceptions::LockWaitTimeoutError,
|
154
|
+
raise ZK::Exceptions::LockWaitTimeoutError,
|
155
|
+
"timed out waiting for #{timeout.inspect} seconds for deletion of paths. #{status_string}"
|
130
156
|
when INTERRUPTED
|
131
157
|
raise ZK::Exceptions::WakeUpException
|
132
158
|
when ZOO_EXPIRED_SESSION_STATE
|
@@ -136,7 +162,7 @@ module ZK
|
|
136
162
|
when ZOO_CLOSED_STATE
|
137
163
|
raise Zookeeper::Exceptions::ConnectionClosed
|
138
164
|
else
|
139
|
-
raise "Hit unexpected case in block_until_node_deleted, result was: #{@result.inspect}"
|
165
|
+
raise "Hit unexpected case in block_until_node_deleted, result was: #{@result.inspect}. #{status_string}"
|
140
166
|
end
|
141
167
|
end
|
142
168
|
ensure
|
@@ -144,6 +170,10 @@ module ZK
|
|
144
170
|
end
|
145
171
|
|
146
172
|
private
|
173
|
+
def status_string
|
174
|
+
"paths: #{paths.inspect}, remaining: #{remaining_paths.inspect}, options: #{options.inspect}"
|
175
|
+
end
|
176
|
+
|
147
177
|
# this method must be synchronized on @mutex, obviously
|
148
178
|
def wait_for_result(timeout)
|
149
179
|
# do the deadline maths
|
@@ -172,7 +202,9 @@ module ZK
|
|
172
202
|
end
|
173
203
|
|
174
204
|
def register_callbacks
|
175
|
-
|
205
|
+
paths.each do |path|
|
206
|
+
@subs << zk.register(path, &method(:node_deletion_cb))
|
207
|
+
end
|
176
208
|
|
177
209
|
[:expired_session, :connecting, :closed].each do |sym|
|
178
210
|
@subs << zk.event_handler.register_state_handler(sym, &method(:session_cb))
|
@@ -183,14 +215,8 @@ module ZK
|
|
183
215
|
@mutex.synchronize do
|
184
216
|
return if @result
|
185
217
|
|
186
|
-
if event.node_deleted?
|
187
|
-
|
188
|
-
@cond.broadcast
|
189
|
-
else
|
190
|
-
unless zk.exists?(path, :watch => true)
|
191
|
-
@result = :deleted
|
192
|
-
@cond.broadcast
|
193
|
-
end
|
218
|
+
if event.node_deleted? or not zk.exists?(event.path, :watch => true)
|
219
|
+
finish_node(event.path)
|
194
220
|
end
|
195
221
|
end
|
196
222
|
end
|
@@ -202,6 +228,36 @@ module ZK
|
|
202
228
|
@cond.broadcast
|
203
229
|
end
|
204
230
|
end
|
205
|
-
end
|
206
|
-
end
|
207
231
|
|
232
|
+
# must be synchronized on @mutex
|
233
|
+
def threshold_met?
|
234
|
+
return true if remaining_paths.size <= threshold
|
235
|
+
end
|
236
|
+
|
237
|
+
# ensures that threshold + 1 nodes are being watched
|
238
|
+
def watch_appropriate_nodes
|
239
|
+
remaining_paths.last( threshold + 1 ).reverse_each do |path|
|
240
|
+
next if watched_paths.include? path
|
241
|
+
watched_paths << path
|
242
|
+
finish_node(path) unless zk.exists?(path, :watch => true)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# must be synchronized on @mutex
|
247
|
+
def finish_blocking
|
248
|
+
@result = :deleted
|
249
|
+
@blocked = NOT_ANYMORE
|
250
|
+
@cond.broadcast # wake any waiting threads
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
254
|
+
def finish_node(path)
|
255
|
+
remaining_paths.delete path
|
256
|
+
watched_paths.delete path
|
257
|
+
|
258
|
+
watch_appropriate_nodes
|
259
|
+
|
260
|
+
finish_blocking if threshold_met?
|
261
|
+
end
|
262
|
+
end # MultiNodeDeletionWatcher
|
263
|
+
end # ZK
|
data/lib/zk/version.rb
CHANGED
@@ -9,16 +9,21 @@ shared_context 'locker non-chrooted' do
|
|
9
9
|
|
10
10
|
let(:path) { "lock_path" }
|
11
11
|
let(:root_lock_path) { "#{ZK::Locker.default_root_lock_node}/#{path}" }
|
12
|
+
let(:semaphore_root_path) { "#{ZK::Locker::Semaphore.default_root_node}/#{path}" }
|
12
13
|
|
13
14
|
before do
|
14
15
|
wait_until{ connections.all?(&:connected?) }
|
15
16
|
zk.rm_rf(ZK::Locker.default_root_lock_node)
|
17
|
+
zk.rm_rf(ZK::Locker::Semaphore.default_root_node)
|
16
18
|
end
|
17
19
|
|
18
20
|
after do
|
19
21
|
connections.each { |c| c.close! }
|
20
22
|
wait_until { !connections.any?(&:connected?) }
|
21
|
-
ZK.open(*connection_args)
|
23
|
+
ZK.open(*connection_args) do |z|
|
24
|
+
z.rm_rf(ZK::Locker.default_root_lock_node)
|
25
|
+
z.rm_rf(ZK::Locker::Semaphore.default_root_node)
|
26
|
+
end
|
22
27
|
end
|
23
28
|
end
|
24
29
|
|
@@ -33,6 +38,7 @@ shared_context 'locker chrooted' do
|
|
33
38
|
let(:zk3) { ZK.new("#{connection_host}#{chroot_path}", connection_opts) }
|
34
39
|
let(:connections) { [zk, zk2, zk3] }
|
35
40
|
let(:root_lock_path) { "#{ZK::Locker.default_root_lock_node}/#{path}" }
|
41
|
+
let(:semaphore_root_path) { "#{ZK::Locker::Semaphore.default_root_node}/#{path}" }
|
36
42
|
|
37
43
|
before do
|
38
44
|
ZK.open(*connection_args) do |zk|
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples_for 'ZK::Locker::Semaphore' do
|
4
|
+
let(:semaphore_size){ 2 }
|
5
|
+
let(:locker) { ZK::Locker::Semaphore.new(zk, path, semaphore_size) }
|
6
|
+
let(:locker2) { ZK::Locker::Semaphore.new(zk2, path, semaphore_size) }
|
7
|
+
let(:locker3) { ZK::Locker::Semaphore.new(zk3, path, semaphore_size) }
|
8
|
+
|
9
|
+
describe :assert! do
|
10
|
+
it_should_behave_like 'LockerBase#assert!'
|
11
|
+
end
|
12
|
+
|
13
|
+
describe :acquirable? do
|
14
|
+
describe %[with default options] do
|
15
|
+
it %[should work if the lock root doesn't exist] do
|
16
|
+
zk.rm_rf(ZK::Locker::Semaphore.default_root_node)
|
17
|
+
locker.should be_acquirable
|
18
|
+
end
|
19
|
+
|
20
|
+
it %[should check local state of lockedness] do
|
21
|
+
locker.lock.should be_true
|
22
|
+
locker.should be_acquirable
|
23
|
+
end
|
24
|
+
|
25
|
+
it %[should check if any participants would prevent us from acquiring the lock] do
|
26
|
+
locker3.lock.should be_true
|
27
|
+
locker.should be_acquirable # total locks given less than semaphore_size
|
28
|
+
locker2.lock.should be_true
|
29
|
+
locker.should_not be_acquirable # total locks given equal to semaphore size
|
30
|
+
locker3.unlock
|
31
|
+
locker.should be_acquirable # total locks given less than semaphore_size
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe :lock do
|
37
|
+
describe 'non-blocking success' do
|
38
|
+
before do
|
39
|
+
@rval = locker.lock
|
40
|
+
@rval2 = locker2.lock
|
41
|
+
end
|
42
|
+
|
43
|
+
it %[should acquire the first lock] do
|
44
|
+
@rval.should be_true
|
45
|
+
locker.should be_locked
|
46
|
+
end
|
47
|
+
|
48
|
+
it %[should acquire the second lock] do
|
49
|
+
@rval2.should be_true
|
50
|
+
locker2.should be_locked
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'non-blocking failure' do
|
55
|
+
before do
|
56
|
+
zk.mkdir_p(semaphore_root_path)
|
57
|
+
semaphore_size.times do
|
58
|
+
zk.create("#{semaphore_root_path}/#{ZK::Locker::SEMAPHORE_LOCK_PREFIX}", '', :mode => :ephemeral_sequential)
|
59
|
+
end
|
60
|
+
@rval = locker.lock
|
61
|
+
end
|
62
|
+
|
63
|
+
it %[should return false] do
|
64
|
+
@rval.should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
it %[should not be locked] do
|
68
|
+
locker.should_not be_locked
|
69
|
+
end
|
70
|
+
|
71
|
+
it %[should not have a lock_path] do
|
72
|
+
locker.lock_path.should be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context do
|
77
|
+
before do
|
78
|
+
zk.mkdir_p(semaphore_root_path)
|
79
|
+
@existing_locks = semaphore_size.times.map do
|
80
|
+
zk.create("#{semaphore_root_path}/#{ZK::Locker::SEMAPHORE_LOCK_PREFIX}", '', :mode => :ephemeral_sequential)
|
81
|
+
end
|
82
|
+
@exc = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'blocking success' do
|
86
|
+
|
87
|
+
it %[should acquire the lock after the write lock is released] do
|
88
|
+
ary = []
|
89
|
+
|
90
|
+
locker.lock.should be_false
|
91
|
+
|
92
|
+
th = Thread.new do
|
93
|
+
locker.lock(:wait => true)
|
94
|
+
ary << :locked
|
95
|
+
end
|
96
|
+
|
97
|
+
locker.wait_until_blocked(5)
|
98
|
+
locker.should be_waiting
|
99
|
+
locker.should_not be_locked
|
100
|
+
ary.should be_empty
|
101
|
+
|
102
|
+
zk.delete(@existing_locks.shuffle.first)
|
103
|
+
|
104
|
+
th.join(2).should == th
|
105
|
+
|
106
|
+
ary.should_not be_empty
|
107
|
+
ary.length.should == 1
|
108
|
+
|
109
|
+
locker.should be_locked
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'blocking timeout' do
|
114
|
+
it %[should raise LockWaitTimeoutError] do
|
115
|
+
ary = []
|
116
|
+
|
117
|
+
write_lock_dir = File.dirname(@existing_locks.first)
|
118
|
+
|
119
|
+
zk.children(write_lock_dir).length.should == semaphore_size
|
120
|
+
|
121
|
+
locker.lock.should be_false
|
122
|
+
|
123
|
+
th = Thread.new do
|
124
|
+
begin
|
125
|
+
locker.lock(:wait => 0.01)
|
126
|
+
ary << :locked
|
127
|
+
rescue Exception => e
|
128
|
+
@exc = e
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
locker.wait_until_blocked(5)
|
133
|
+
locker.should be_waiting
|
134
|
+
locker.should_not be_locked
|
135
|
+
ary.should be_empty
|
136
|
+
|
137
|
+
th.join(2).should == th
|
138
|
+
|
139
|
+
zk.children(write_lock_dir).length.should == semaphore_size
|
140
|
+
|
141
|
+
ary.should be_empty
|
142
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end # context
|
147
|
+
end # lock
|
148
|
+
|
149
|
+
describe :unlock do
|
150
|
+
it_should_behave_like 'LockerBase#unlock'
|
151
|
+
end
|
152
|
+
end # SharedLocker
|
153
|
+
|
154
|
+
|
155
|
+
describe do
|
156
|
+
include_context 'locker non-chrooted'
|
157
|
+
|
158
|
+
it_should_behave_like 'ZK::Locker::Semaphore'
|
159
|
+
end
|
160
|
+
|
161
|
+
describe :chrooted => true do
|
162
|
+
include_context 'locker chrooted'
|
163
|
+
|
164
|
+
it_should_behave_like 'ZK::Locker::Semaphore'
|
165
|
+
end
|
@@ -42,6 +42,53 @@ shared_examples_for :shared_exclusive_integration do
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
describe 'multiple shared locks acquired first' do
|
46
|
+
before do
|
47
|
+
zk3.should_not be_nil
|
48
|
+
@sh_lock2 = ZK::Locker.shared_locker(zk3, path)
|
49
|
+
end
|
50
|
+
it %[should not aquire a lock when highest-numbered released but others remain] do
|
51
|
+
@sh_lock.lock.should be_true
|
52
|
+
@sh_lock2.lock.should be_true
|
53
|
+
@ex_lock.lock.should be_false
|
54
|
+
|
55
|
+
mutex = Monitor.new
|
56
|
+
cond = mutex.new_cond
|
57
|
+
|
58
|
+
th = Thread.new do
|
59
|
+
logger.debug { "@ex_lock trying to acquire acquire lock" }
|
60
|
+
begin
|
61
|
+
@ex_lock.with_lock(:wait=>0.1) do
|
62
|
+
th[:got_lock] = @ex_lock.locked?
|
63
|
+
logger.debug { "@ex_lock.locked? #{@ex_lock.locked?}" }
|
64
|
+
end
|
65
|
+
rescue ZK::Exceptions::LockWaitTimeoutError
|
66
|
+
logger.debug { "@ex_lock timed out trying to acquire acquire lock" }
|
67
|
+
th[:got_lock] = false
|
68
|
+
rescue
|
69
|
+
logger.debug { "@ex_lock raised unexpected error: #{$!.inspext}" }
|
70
|
+
th[:got_lock] = {:error_raised => $!}
|
71
|
+
end
|
72
|
+
mutex.synchronize { cond.broadcast }
|
73
|
+
end
|
74
|
+
|
75
|
+
mutex.synchronize do
|
76
|
+
@ex_lock.wait_until_blocked(1)
|
77
|
+
logger.debug { "unlocking the highest shared lock" }
|
78
|
+
@sh_lock2.unlock.should be_true
|
79
|
+
cond.wait_until { (!th[:got_lock].nil?) } # make sure they actually received the lock (avoid race)
|
80
|
+
th[:got_lock].should be_false
|
81
|
+
logger.debug { "they didn't get the lock." }
|
82
|
+
end
|
83
|
+
|
84
|
+
th.join(5).should == th
|
85
|
+
|
86
|
+
logger.debug { "thread joined, exclusive lock should be releasd" }
|
87
|
+
@sh_lock.unlock.should be_true
|
88
|
+
@ex_lock.should_not be_locked
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
45
92
|
describe 'exclusive lock acquired first' do
|
46
93
|
it %[should block shared lock from acquiring until released] do
|
47
94
|
@ex_lock.lock.should be_true
|
@@ -86,6 +86,107 @@ describe ZK::NodeDeletionWatcher do
|
|
86
86
|
@n.should be_done
|
87
87
|
end
|
88
88
|
end
|
89
|
-
end
|
90
89
|
|
90
|
+
context %[multiple nodes] do
|
91
|
+
let(:watcher_params){ Hash.new }
|
92
|
+
let(:paths) do
|
93
|
+
[
|
94
|
+
"#{@base_path}/node_deleteion_watcher_victim_one",
|
95
|
+
"#{@base_path}/node_deleteion_watcher_victim_two",
|
96
|
+
"#{@base_path}/node_deleteion_watcher_victim_three"
|
97
|
+
]
|
98
|
+
end
|
99
|
+
let(:created_paths){ paths }
|
100
|
+
let(:paths_to_delete){ paths }
|
101
|
+
let(:watcher_args) do
|
102
|
+
args = [@zk, paths]
|
103
|
+
args << watcher_params unless watcher_params.nil?
|
104
|
+
args
|
105
|
+
end
|
106
|
+
let(:watcher){ ZK::NodeDeletionWatcher.new(*watcher_args) }
|
107
|
+
subject{ watcher }
|
91
108
|
|
109
|
+
before(:each) do
|
110
|
+
created_paths.each do |path|
|
111
|
+
@zk.mkdir_p(path)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
let(:watcher_block_params){ Hash.new }
|
116
|
+
let(:watcher_wait_timeout){ 5 }
|
117
|
+
|
118
|
+
let(:runner) do
|
119
|
+
Thread.new do
|
120
|
+
Thread.pass
|
121
|
+
begin
|
122
|
+
watcher.block_until_deleted( watcher_block_params )
|
123
|
+
rescue Object
|
124
|
+
@exc = $!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
let(:controller) do
|
130
|
+
Thread.new do
|
131
|
+
Thread.pass
|
132
|
+
watcher.wait_until_blocked( watcher_wait_timeout )
|
133
|
+
paths_to_delete.each do |path|
|
134
|
+
@zk.rm_rf(path)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should block until all are deleted' do
|
140
|
+
runner.run
|
141
|
+
controller.run
|
142
|
+
controller.join
|
143
|
+
runner.join(5).should == runner
|
144
|
+
watcher.should be_done
|
145
|
+
end
|
146
|
+
|
147
|
+
context %[threshold not supplied] do
|
148
|
+
let(:watcher_params){}
|
149
|
+
|
150
|
+
it 'should raise' do
|
151
|
+
expect{ watcher }.to_not raise_exception
|
152
|
+
end
|
153
|
+
its(:threshold){ should be_zero }
|
154
|
+
end
|
155
|
+
|
156
|
+
context %[invalid threshold given] do
|
157
|
+
let(:watcher_params){ {:threshold => :foo} }
|
158
|
+
it 'should raise' do
|
159
|
+
expect{ watcher }.to raise_exception
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context %[threshold of 1] do
|
164
|
+
let(:watcher_params) { { :threshold => 1 } }
|
165
|
+
context do
|
166
|
+
let(:paths_to_delete) { paths.first(2) }
|
167
|
+
it 'should release when 1 remains' do
|
168
|
+
runner.run
|
169
|
+
controller.run
|
170
|
+
controller.join
|
171
|
+
runner.join(5).should == runner
|
172
|
+
watcher.should be_done
|
173
|
+
end
|
174
|
+
its(:threshold){ should == 1 }
|
175
|
+
end
|
176
|
+
|
177
|
+
context do
|
178
|
+
let(:paths_to_delete) { paths.first(1) }
|
179
|
+
let(:watcher_block_params){ { :timeout => 0.02 } }
|
180
|
+
it 'should raise when 2 remain' do
|
181
|
+
runner.run
|
182
|
+
controller.run
|
183
|
+
controller.join
|
184
|
+
runner.join(5).should == runner
|
185
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
186
|
+
watcher.should be_done
|
187
|
+
watcher.should be_timed_out
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 51
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
8
|
+
- 9
|
9
9
|
- 0
|
10
|
-
version: 1.
|
10
|
+
version: 1.9.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jonathan D. Simms
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2013-
|
19
|
+
date: 2013-08-07 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: zookeeper
|
@@ -64,6 +64,8 @@ extra_rdoc_files: []
|
|
64
64
|
files:
|
65
65
|
- .dotfiles/ctags_paths
|
66
66
|
- .dotfiles/rspec-logging
|
67
|
+
- .dotfiles/ruby-gemset
|
68
|
+
- .dotfiles/ruby-version
|
67
69
|
- .dotfiles/rvmrc
|
68
70
|
- .gitignore
|
69
71
|
- .gitmodules
|
@@ -101,6 +103,7 @@ files:
|
|
101
103
|
- lib/zk/locker/exclusive_locker.rb
|
102
104
|
- lib/zk/locker/lock_options.rb
|
103
105
|
- lib/zk/locker/locker_base.rb
|
106
|
+
- lib/zk/locker/semaphore.rb
|
104
107
|
- lib/zk/locker/shared_locker.rb
|
105
108
|
- lib/zk/logging.rb
|
106
109
|
- lib/zk/message_queue.rb
|
@@ -146,6 +149,7 @@ files:
|
|
146
149
|
- spec/zk/extensions_spec.rb
|
147
150
|
- spec/zk/locker/exclusive_locker_spec.rb
|
148
151
|
- spec/zk/locker/locker_basic_spec.rb
|
152
|
+
- spec/zk/locker/semaphore_spec.rb
|
149
153
|
- spec/zk/locker/shared_exclusive_integration_spec.rb
|
150
154
|
- spec/zk/locker/shared_locker_spec.rb
|
151
155
|
- spec/zk/locker_spec.rb
|
@@ -224,6 +228,7 @@ test_files:
|
|
224
228
|
- spec/zk/extensions_spec.rb
|
225
229
|
- spec/zk/locker/exclusive_locker_spec.rb
|
226
230
|
- spec/zk/locker/locker_basic_spec.rb
|
231
|
+
- spec/zk/locker/semaphore_spec.rb
|
227
232
|
- spec/zk/locker/shared_exclusive_integration_spec.rb
|
228
233
|
- spec/zk/locker/shared_locker_spec.rb
|
229
234
|
- spec/zk/locker_spec.rb
|