zk 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status (master)](https://secure.travis-ci.org/
|
3
|
+
[![Build Status (master)](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
|