zk 1.6.5 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +27 -19
- data/RELEASES.markdown +37 -0
- data/lib/zk/client/conveniences.rb +23 -5
- data/lib/zk/exceptions.rb +4 -0
- data/lib/zk/locker.rb +1 -0
- data/lib/zk/locker/exclusive_locker.rb +19 -14
- data/lib/zk/locker/lock_options.rb +36 -0
- data/lib/zk/locker/locker_base.rb +53 -11
- data/lib/zk/locker/shared_locker.rb +23 -19
- data/lib/zk/node_deletion_watcher.rb +48 -7
- data/lib/zk/version.rb +1 -1
- data/spec/zk/locker/exclusive_locker_spec.rb +55 -3
- data/spec/zk/locker/locker_basic_spec.rb +81 -15
- data/spec/zk/locker/shared_locker_spec.rb +73 -18
- data/spec/zk/node_deletion_watcher_spec.rb +24 -2
- metadata +6 -5
data/README.markdown
CHANGED
@@ -65,41 +65,49 @@ In addition to all of that, I would like to think that the public API the ZK::Cl
|
|
65
65
|
[zk-eventmachine]: https://github.com/slyphon/zk-eventmachine
|
66
66
|
|
67
67
|
## NEWS ##
|
68
|
-
### v1.
|
68
|
+
### v1.7.0 ###
|
69
69
|
|
70
|
-
*
|
71
|
-
* *Fix for use in resque!* A small bug was preventing resque from activating the fork hook.
|
70
|
+
* Added Locker timeout feature for blocking calls. (issue #40)
|
72
71
|
|
73
|
-
|
72
|
+
Previously, when dealing with locks, there were only two options: blocking or non-blocking. In order to come up with a time-limited lock, you had to poll every so often until you acquired the lock. This is, needless to say, both inefficient and doesn't allow for fair acquisition.
|
74
73
|
|
75
|
-
|
74
|
+
A timeout option has been added so that when blocking waiting for a lock, you can specify a deadline by which the lock should have been acquired.
|
76
75
|
|
77
|
-
|
76
|
+
```ruby
|
77
|
+
zk = ZK.new
|
78
78
|
|
79
|
-
|
79
|
+
locker = zk.locker('lock name')
|
80
80
|
|
81
|
-
|
81
|
+
begin
|
82
|
+
locker.lock(:wait => 5.0) # wait up to 5.0 seconds to acquire the lock
|
83
|
+
rescue ZK::Exceptions::LockWaitTimeoutError
|
84
|
+
$stderr.puts "could not acquire the lock in time"
|
85
|
+
end
|
86
|
+
```
|
82
87
|
|
83
|
-
|
88
|
+
Also available when using the convenience `#with_lock` methods
|
84
89
|
|
85
90
|
```ruby
|
86
|
-
ZK.open('localhost:2181') do |zk|
|
87
|
-
ZK::Locker.cleanup(zk)
|
88
|
-
end
|
89
|
-
```
|
90
91
|
|
91
|
-
|
92
|
+
zk = ZK.new
|
92
93
|
|
93
|
-
|
94
|
+
begin
|
95
|
+
zk.with_lock('lock name', :wait => 5.0) do |lock|
|
96
|
+
# do stuff while holding lock
|
97
|
+
end
|
98
|
+
rescue ZK::Exceptions::LockWaitTimeoutError
|
99
|
+
$stderr.puts "could not acquire the lock in time"
|
100
|
+
end
|
94
101
|
|
95
|
-
|
102
|
+
```
|
96
103
|
|
97
|
-
### v1.
|
104
|
+
### v1.6.4 ###
|
98
105
|
|
99
|
-
*
|
106
|
+
* Remove unnecessary dependency on backports gem
|
107
|
+
* *Fix for use in resque!* A small bug was preventing resque from activating the fork hook.
|
100
108
|
|
101
|
-
* 'private' is not 'protected'. I've been writing ruby for several years now, and apparently I'd forgotten that 'protected' does not work like how it does in java. The visibility of these methods has been corrected, and all specs pass, so I don't expect issues...but please report if this change causes any bugs in user code.
|
102
109
|
|
110
|
+
See the [RELEASES][] page for more info on features and bugfixes in each release.
|
103
111
|
|
104
112
|
## Caveats
|
105
113
|
|
data/RELEASES.markdown
CHANGED
@@ -1,5 +1,42 @@
|
|
1
1
|
This file notes feature differences and bugfixes contained between releases.
|
2
2
|
|
3
|
+
### v1.7.0 ###
|
4
|
+
|
5
|
+
* Added Locker timeout feature for blocking calls. (issue #40)
|
6
|
+
|
7
|
+
Previously, when dealing with locks, there were only two options: blocking or non-blocking. In order to come up with a time-limited lock, you had to poll every so often until you acquired the lock. This is, needless to say, both inefficient and doesn't allow for fair acquisition.
|
8
|
+
|
9
|
+
A timeout option has been added so that when blocking waiting for a lock, you can specify a deadline by which the lock should have been acquired.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
zk = ZK.new
|
13
|
+
|
14
|
+
locker = zk.locker('lock name')
|
15
|
+
|
16
|
+
begin
|
17
|
+
locker.lock(:wait => 5.0) # wait up to 5.0 seconds to acquire the lock
|
18
|
+
rescue ZK::Exceptions::LockWaitTimeoutError
|
19
|
+
$stderr.puts "could not acquire the lock in time"
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
Also available when using the convenience `#with_lock` methods
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
|
27
|
+
zk = ZK.new
|
28
|
+
|
29
|
+
begin
|
30
|
+
zk.with_lock('lock name', :wait => 5.0) do |lock|
|
31
|
+
# do stuff while holding lock
|
32
|
+
end
|
33
|
+
rescue ZK::Exceptions::LockWaitTimeoutError
|
34
|
+
$stderr.puts "could not acquire the lock in time"
|
35
|
+
end
|
36
|
+
|
37
|
+
```
|
38
|
+
|
39
|
+
|
3
40
|
### v1.6.4 ###
|
4
41
|
|
5
42
|
* Remove unnecessary dependency on backports gem
|
@@ -66,6 +66,9 @@ module ZK
|
|
66
66
|
# will block the caller until the lock is acquired, and release the lock
|
67
67
|
# when the block is exited.
|
68
68
|
#
|
69
|
+
# Options are the same as for {Locker::LockerBase#lock #lock} with the addition of
|
70
|
+
# `:mode`, documented below.
|
71
|
+
#
|
69
72
|
# @param name (see #locker)
|
70
73
|
#
|
71
74
|
# @option opts [:shared,:exclusive] :mode (:exclusive) the type of lock
|
@@ -73,25 +76,40 @@ module ZK
|
|
73
76
|
#
|
74
77
|
# @return the return value of the given block
|
75
78
|
#
|
76
|
-
# @yield calls the block once the lock has been acquired
|
79
|
+
# @yield [lock] calls the block once the lock has been acquired with the
|
80
|
+
# lock instance
|
77
81
|
#
|
78
82
|
# @example
|
79
83
|
#
|
80
|
-
# zk.with_lock('foo') do
|
84
|
+
# zk.with_lock('foo') do |lock|
|
81
85
|
# # this code is executed while holding the lock
|
82
86
|
# end
|
83
87
|
#
|
88
|
+
# @example with timeout
|
89
|
+
#
|
90
|
+
# begin
|
91
|
+
# zk.with_lock('foo', :wait => 5.0) do |lock|
|
92
|
+
# # this code is executed while holding the lock
|
93
|
+
# end
|
94
|
+
# rescue ZK::Exceptions::LockWaitTimeoutError
|
95
|
+
# $stderr.puts "we didn't acquire the lock in time"
|
96
|
+
# end
|
97
|
+
#
|
84
98
|
# @raise [ArgumentError] if `opts[:mode]` is not one of the expected values
|
85
99
|
#
|
100
|
+
# @raise [ZK::Exceptions::LockWaitTimeoutError] if :wait timeout is
|
101
|
+
# exceeded without acquiring the lock
|
102
|
+
#
|
86
103
|
def with_lock(name, opts={}, &b)
|
87
|
-
|
104
|
+
opts = opts.dup
|
105
|
+
mode = opts.delete(:mode) { |_| :exclusive }
|
88
106
|
|
89
107
|
raise ArgumentError, ":mode option must be either :shared or :exclusive, not #{mode.inspect}" unless [:shared, :exclusive].include?(mode)
|
90
108
|
|
91
109
|
if mode == :shared
|
92
|
-
shared_locker(name).with_lock(&b)
|
110
|
+
shared_locker(name).with_lock(opts, &b)
|
93
111
|
else
|
94
|
-
locker(name).with_lock(&b)
|
112
|
+
locker(name).with_lock(opts, &b)
|
95
113
|
end
|
96
114
|
end
|
97
115
|
|
data/lib/zk/exceptions.rb
CHANGED
@@ -141,6 +141,7 @@ module ZK
|
|
141
141
|
# called when the client is reopened, resumed, or paused when in an invalid state
|
142
142
|
class InvalidStateError < ZKError; end
|
143
143
|
|
144
|
+
# Raised when a NodeDeletionWatcher is interrupted by another thread
|
144
145
|
class WakeUpException < ZKError; end
|
145
146
|
|
146
147
|
# raised when a chrooted conection is requested but the root doesn't exist
|
@@ -155,6 +156,9 @@ module ZK
|
|
155
156
|
super("Chroot strings must start with a '/' you provided: #{erroneous_string.inspect}")
|
156
157
|
end
|
157
158
|
end
|
159
|
+
|
160
|
+
# raised when we are blocked waiting on a lock and the timeout expires
|
161
|
+
class LockWaitTimeoutError < ZKError; end
|
158
162
|
end
|
159
163
|
end
|
160
164
|
|
data/lib/zk/locker.rb
CHANGED
@@ -17,18 +17,8 @@ module ZK
|
|
17
17
|
# (see LockerBase#lock)
|
18
18
|
# obtain an exclusive lock.
|
19
19
|
#
|
20
|
-
def lock(
|
21
|
-
|
22
|
-
create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
|
23
|
-
|
24
|
-
if got_write_lock?
|
25
|
-
synchronize { @locked = true }
|
26
|
-
elsif blocking
|
27
|
-
block_until_write_lock!
|
28
|
-
else
|
29
|
-
cleanup_lock_path!
|
30
|
-
false
|
31
|
-
end
|
20
|
+
def lock(opts={})
|
21
|
+
super
|
32
22
|
end
|
33
23
|
|
34
24
|
# (see LockerBase#assert!)
|
@@ -54,6 +44,21 @@ module ZK
|
|
54
44
|
end
|
55
45
|
|
56
46
|
private
|
47
|
+
def lock_with_opts_hash(opts)
|
48
|
+
create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
|
49
|
+
|
50
|
+
lock_opts = LockOptions.new(opts)
|
51
|
+
|
52
|
+
if got_write_lock?
|
53
|
+
@mutex.synchronize { @locked = true }
|
54
|
+
elsif lock_opts.blocking?
|
55
|
+
block_until_write_lock!(:timeout => lock_opts.timeout)
|
56
|
+
else
|
57
|
+
cleanup_lock_path!
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
57
62
|
# the node that is next-lowest in sequence number to ours, the one we
|
58
63
|
# watch for updates to
|
59
64
|
def next_lowest_node
|
@@ -70,7 +75,7 @@ module ZK
|
|
70
75
|
end
|
71
76
|
alias got_lock? got_write_lock?
|
72
77
|
|
73
|
-
def block_until_write_lock!
|
78
|
+
def block_until_write_lock!(opts={})
|
74
79
|
begin
|
75
80
|
path = "#{root_lock_path}/#{next_lowest_node}"
|
76
81
|
logger.debug { "#{self.class}##{__method__} path=#{path.inspect}" }
|
@@ -85,7 +90,7 @@ module ZK
|
|
85
90
|
logger.debug { "calling block_until_deleted" }
|
86
91
|
Thread.pass
|
87
92
|
|
88
|
-
@node_deletion_watcher.block_until_deleted
|
93
|
+
@node_deletion_watcher.block_until_deleted(opts)
|
89
94
|
rescue WeAreTheLowestLockNumberException
|
90
95
|
ensure
|
91
96
|
logger.debug { "block_until_deleted returned" }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ZK
|
2
|
+
module Locker
|
3
|
+
# @private
|
4
|
+
class LockOptions
|
5
|
+
attr_reader :wait, :now, :timeout
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
@timeout = nil
|
9
|
+
@now = Time.now
|
10
|
+
|
11
|
+
raise "BLAH!" if opts.has_key?(:block)
|
12
|
+
|
13
|
+
if opts.has_key?(:timeout)
|
14
|
+
raise ArgumentError, ":timeout is an invalid option, use :wait with a numeric argument"
|
15
|
+
end
|
16
|
+
|
17
|
+
case w = opts[:wait]
|
18
|
+
when TrueClass, FalseClass, nil
|
19
|
+
@wait = false|w
|
20
|
+
when Numeric
|
21
|
+
if w < 0
|
22
|
+
raise ArgumentError, ":wait must be a positive float or integer, or zero, not: #{w.inspect}"
|
23
|
+
end
|
24
|
+
@wait = true
|
25
|
+
@timeout = w.to_f
|
26
|
+
else
|
27
|
+
raise ArgumentError, ":wait must be true, false, nil, or Numeric, not #{w.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def blocking?
|
32
|
+
@wait
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -65,9 +65,24 @@ module ZK
|
|
65
65
|
#
|
66
66
|
# there is no non-blocking version of this method
|
67
67
|
#
|
68
|
-
|
69
|
-
|
70
|
-
|
68
|
+
# @yield [lock] calls the block with the lock instance when acquired
|
69
|
+
#
|
70
|
+
# @option opts [Numeric,true] :wait (nil) if non-nil, the amount of time to
|
71
|
+
# wait for the lock to be acquired. since with_lock is only blocking,
|
72
|
+
# `false` isn't a valid option. `true` is ignored (as it is the default).
|
73
|
+
# If a Numeric (float or integer) option is given, maximum amount of time
|
74
|
+
# to wait for lock acquisition.
|
75
|
+
#
|
76
|
+
# @raise [LockWaitTimeoutError] if the :wait timeout is exceeded
|
77
|
+
# @raise [ArgumentError] if :wait is false (since you can't do non-blocking)
|
78
|
+
def with_lock(opts={})
|
79
|
+
if opts[:wait].kind_of?(FalseClass)
|
80
|
+
raise ArgumentError, ":wait cannot be false, with_lock is only used in blocking mode"
|
81
|
+
end
|
82
|
+
|
83
|
+
opts = { :wait => true }.merge(opts)
|
84
|
+
lock(opts)
|
85
|
+
yield self
|
71
86
|
ensure
|
72
87
|
unlock
|
73
88
|
end
|
@@ -148,8 +163,20 @@ module ZK
|
|
148
163
|
unlock
|
149
164
|
end
|
150
165
|
|
151
|
-
# @
|
152
|
-
#
|
166
|
+
# @overload lock(blocking=false)
|
167
|
+
# @param blocking [true,false] if true we block the caller until we can
|
168
|
+
# obtain a lock on the resource
|
169
|
+
#
|
170
|
+
# @deprecated in favor of the options hash style
|
171
|
+
#
|
172
|
+
# @overload lock(opts={})
|
173
|
+
# @option opts [true,false,Numeric] :wait (false) If true we block the
|
174
|
+
# caller until we obtain a lock on the resource. If false, we do not
|
175
|
+
# block. If a Numeric, the number of seconds we should wait for the
|
176
|
+
# lock to be acquired. Will raise LockWaitTimeoutError if we exceed
|
177
|
+
# the timeout.
|
178
|
+
#
|
179
|
+
# @since 1.7
|
153
180
|
#
|
154
181
|
# @return [true] if we're already obtained a shared lock, or if we were able to
|
155
182
|
# obtain the lock in non-blocking mode.
|
@@ -161,16 +188,27 @@ module ZK
|
|
161
188
|
# @raise [InterruptedSession] raised when blocked waiting for a lock and
|
162
189
|
# the underlying client's session is interrupted.
|
163
190
|
#
|
164
|
-
# @
|
165
|
-
|
166
|
-
|
191
|
+
# @raise [LockWaitTimeoutError] if the given timeout is exceeded waiting
|
192
|
+
# for the lock to be acquired
|
193
|
+
#
|
194
|
+
# @see ZK::Client::Unixisms#block_until_node_deleted for more about possible execptions
|
195
|
+
def lock(opts={})
|
196
|
+
return true if @mutex.synchronize { @locked }
|
197
|
+
|
198
|
+
case opts
|
199
|
+
when TrueClass, FalseClass # old style boolean argument
|
200
|
+
opts = { :wait => opts }
|
201
|
+
end
|
202
|
+
|
203
|
+
lock_with_opts_hash(opts)
|
167
204
|
end
|
168
205
|
|
169
|
-
#
|
206
|
+
# delegates to {#lock}
|
207
|
+
#
|
170
208
|
# @deprecated the use of lock! is deprecated and may be removed or have
|
171
209
|
# its semantics changed in a future release
|
172
|
-
def lock!(
|
173
|
-
lock(
|
210
|
+
def lock!(opts={})
|
211
|
+
lock(opts)
|
174
212
|
end
|
175
213
|
|
176
214
|
# returns true if this locker is waiting to acquire lock
|
@@ -243,6 +281,10 @@ module ZK
|
|
243
281
|
end
|
244
282
|
|
245
283
|
private
|
284
|
+
def lock_with_opts_hash(opts={})
|
285
|
+
raise NotImplementedError
|
286
|
+
end
|
287
|
+
|
246
288
|
def synchronize
|
247
289
|
@mutex.synchronize { yield }
|
248
290
|
end
|
@@ -6,20 +6,8 @@ module ZK
|
|
6
6
|
# (see LockerBase#lock)
|
7
7
|
# obtain a shared lock.
|
8
8
|
#
|
9
|
-
def lock(
|
10
|
-
|
11
|
-
create_lock_path!(SHARED_LOCK_PREFIX)
|
12
|
-
|
13
|
-
if got_read_lock?
|
14
|
-
@locked = true
|
15
|
-
elsif blocking
|
16
|
-
block_until_read_lock!
|
17
|
-
else
|
18
|
-
# we didn't get the lock, and we're not gonna wait around for it, so
|
19
|
-
# clean up after ourselves
|
20
|
-
cleanup_lock_path!
|
21
|
-
false
|
22
|
-
end
|
9
|
+
def lock(opts={})
|
10
|
+
super
|
23
11
|
end
|
24
12
|
|
25
13
|
# (see LockerBase#assert!)
|
@@ -93,24 +81,40 @@ module ZK
|
|
93
81
|
alias got_lock? got_read_lock?
|
94
82
|
|
95
83
|
private
|
96
|
-
|
97
|
-
|
84
|
+
def lock_with_opts_hash(opts)
|
85
|
+
create_lock_path!(SHARED_LOCK_PREFIX)
|
86
|
+
|
87
|
+
lock_opts = LockOptions.new(opts)
|
88
|
+
|
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
|
+
# we didn't get the lock, and we're not gonna wait around for it, so
|
95
|
+
# clean up after ourselves
|
96
|
+
cleanup_lock_path!
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def block_until_read_lock!(opts={})
|
98
102
|
begin
|
99
103
|
path = "#{root_lock_path}/#{next_lowest_write_lock_name}"
|
100
104
|
logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
|
101
105
|
|
102
|
-
synchronize do
|
106
|
+
@mutex.synchronize do
|
103
107
|
@node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
|
104
108
|
@cond.broadcast
|
105
109
|
end
|
106
110
|
|
107
|
-
@node_deletion_watcher.block_until_deleted
|
111
|
+
@node_deletion_watcher.block_until_deleted(opts)
|
108
112
|
rescue NoWriteLockFoundException
|
109
113
|
# next_lowest_write_lock_name may raise NoWriteLockFoundException,
|
110
114
|
# which means we should not block as we have the lock (there is nothing to wait for)
|
111
115
|
end
|
112
116
|
|
113
|
-
synchronize { @locked = true }
|
117
|
+
@mutex.synchronize { @locked = true }
|
114
118
|
end
|
115
119
|
end # SharedLocker
|
116
120
|
end # Locker
|
@@ -10,6 +10,7 @@ module ZK
|
|
10
10
|
BLOCKED = :yes
|
11
11
|
NOT_ANYMORE = :not_anymore
|
12
12
|
INTERRUPTED = :interrupted
|
13
|
+
TIMED_OUT = :timed_out
|
13
14
|
end
|
14
15
|
include Constants
|
15
16
|
|
@@ -36,6 +37,10 @@ module ZK
|
|
36
37
|
@mutex.synchronize { @blocked == BLOCKED }
|
37
38
|
end
|
38
39
|
|
40
|
+
def timed_out?
|
41
|
+
@mutex.synchronize { @result == TIMED_OUT }
|
42
|
+
end
|
43
|
+
|
39
44
|
# this is for testing, allows us to wait until this object has gone into
|
40
45
|
# blocking state.
|
41
46
|
#
|
@@ -66,7 +71,7 @@ module ZK
|
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
69
|
-
# cause a thread blocked us to be awakened and have a WakeUpException
|
74
|
+
# cause a thread blocked by us to be awakened and have a WakeUpException
|
70
75
|
# raised.
|
71
76
|
#
|
72
77
|
# if a result has already been delivered, then this does nothing
|
@@ -86,7 +91,13 @@ module ZK
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
89
|
-
|
94
|
+
# @option opts [Numeric] :timeout (nil) if a positive integer, represents a duration in
|
95
|
+
# seconds after which, if we have not acquired the lock, a LockWaitTimeoutError will
|
96
|
+
# be raised in all waiting threads
|
97
|
+
#
|
98
|
+
def block_until_deleted(opts={})
|
99
|
+
timeout = opts[:timeout]
|
100
|
+
|
90
101
|
@mutex.synchronize do
|
91
102
|
raise InvalidStateError, "Already fired for #{path}" if @result
|
92
103
|
register_callbacks
|
@@ -103,13 +114,19 @@ module ZK
|
|
103
114
|
|
104
115
|
@blocked = BLOCKED
|
105
116
|
@cond.broadcast # wake threads waiting for @blocked to change
|
106
|
-
|
117
|
+
|
118
|
+
wait_for_result(timeout)
|
119
|
+
|
107
120
|
@blocked = NOT_ANYMORE
|
108
121
|
|
122
|
+
logger.debug { "got result for path: #{path}, result: #{@result.inspect}" }
|
123
|
+
|
109
124
|
case @result
|
110
125
|
when :deleted
|
111
126
|
logger.debug { "path #{path} was deleted" }
|
112
127
|
return true
|
128
|
+
when TIMED_OUT
|
129
|
+
raise ZK::Exceptions::LockWaitTimeoutError, "timed out waiting for #{timeout.inspect} seconds for deletion of path: #{path.inspect}"
|
113
130
|
when INTERRUPTED
|
114
131
|
raise ZK::Exceptions::WakeUpException
|
115
132
|
when ZOO_EXPIRED_SESSION_STATE
|
@@ -127,6 +144,29 @@ module ZK
|
|
127
144
|
end
|
128
145
|
|
129
146
|
private
|
147
|
+
# this method must be synchronized on @mutex, obviously
|
148
|
+
def wait_for_result(timeout)
|
149
|
+
# do the deadline maths
|
150
|
+
time_to_stop = timeout ? (Time.now + timeout) : nil # slight time slippage between here
|
151
|
+
#
|
152
|
+
until @result #
|
153
|
+
if timeout # and here
|
154
|
+
now = Time.now
|
155
|
+
|
156
|
+
if @result
|
157
|
+
return
|
158
|
+
elsif (now >= time_to_stop)
|
159
|
+
@result = TIMED_OUT
|
160
|
+
return
|
161
|
+
end
|
162
|
+
|
163
|
+
@cond.wait(time_to_stop.to_f - now.to_f)
|
164
|
+
else
|
165
|
+
@cond.wait_until { @result }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
130
170
|
def unregister_callbacks
|
131
171
|
@subs.each(&:unregister)
|
132
172
|
end
|
@@ -141,6 +181,8 @@ module ZK
|
|
141
181
|
|
142
182
|
def node_deletion_cb(event)
|
143
183
|
@mutex.synchronize do
|
184
|
+
return if @result
|
185
|
+
|
144
186
|
if event.node_deleted?
|
145
187
|
@result = :deleted
|
146
188
|
@cond.broadcast
|
@@ -155,10 +197,9 @@ module ZK
|
|
155
197
|
|
156
198
|
def session_cb(event)
|
157
199
|
@mutex.synchronize do
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
200
|
+
return if @result
|
201
|
+
@result = event.state
|
202
|
+
@cond.broadcast
|
162
203
|
end
|
163
204
|
end
|
164
205
|
end
|
data/lib/zk/version.rb
CHANGED
@@ -82,13 +82,16 @@ shared_examples_for 'ZK::Locker::ExclusiveLocker' do
|
|
82
82
|
end
|
83
83
|
|
84
84
|
describe 'blocking' do
|
85
|
+
let(:read_lock_path_template) { "/_zklocking/#{path}/#{ZK::Locker::SHARED_LOCK_PREFIX}" }
|
86
|
+
|
85
87
|
before do
|
86
88
|
zk.mkdir_p(root_lock_path)
|
89
|
+
@read_lock_path = zk.create(read_lock_path_template, '', :mode => :ephemeral_sequential)
|
90
|
+
@exc = nil
|
87
91
|
end
|
88
92
|
|
89
|
-
it %[should block waiting for the lock] do
|
93
|
+
it %[should block waiting for the lock with old style lock semantics] do
|
90
94
|
ary = []
|
91
|
-
read_lock_path = zk.create("/_zklocking/#{path}/read", '', :mode => :ephemeral_sequential)
|
92
95
|
|
93
96
|
locker.lock.should be_false
|
94
97
|
|
@@ -102,13 +105,62 @@ shared_examples_for 'ZK::Locker::ExclusiveLocker' do
|
|
102
105
|
ary.should be_empty
|
103
106
|
locker.should_not be_locked
|
104
107
|
|
105
|
-
zk.delete(read_lock_path)
|
108
|
+
zk.delete(@read_lock_path)
|
109
|
+
|
110
|
+
th.join(2).should == th
|
111
|
+
|
112
|
+
ary.length.should == 1
|
113
|
+
locker.should be_locked
|
114
|
+
end
|
115
|
+
|
116
|
+
it %[should block waiting for the lock with new style lock semantics] do
|
117
|
+
ary = []
|
118
|
+
|
119
|
+
locker.lock.should be_false
|
120
|
+
|
121
|
+
th = Thread.new do
|
122
|
+
locker.lock(:wait => true)
|
123
|
+
ary << :locked
|
124
|
+
end
|
125
|
+
|
126
|
+
locker.wait_until_blocked(5)
|
127
|
+
|
128
|
+
ary.should be_empty
|
129
|
+
locker.should_not be_locked
|
130
|
+
|
131
|
+
zk.delete(@read_lock_path)
|
106
132
|
|
107
133
|
th.join(2).should == th
|
108
134
|
|
109
135
|
ary.length.should == 1
|
110
136
|
locker.should be_locked
|
111
137
|
end
|
138
|
+
|
139
|
+
it %[should time out waiting for the lock] do
|
140
|
+
ary = []
|
141
|
+
|
142
|
+
locker.lock.should be_false
|
143
|
+
|
144
|
+
th = Thread.new do
|
145
|
+
begin
|
146
|
+
locker.lock(:wait => 0.01)
|
147
|
+
ary << :locked
|
148
|
+
rescue Exception => e
|
149
|
+
@exc = e
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
locker.wait_until_blocked(5)
|
154
|
+
|
155
|
+
ary.should be_empty
|
156
|
+
locker.should_not be_locked
|
157
|
+
|
158
|
+
th.join(2).should == th
|
159
|
+
|
160
|
+
ary.should be_empty
|
161
|
+
@exc.should_not be_nil
|
162
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
163
|
+
end
|
112
164
|
end # blocking
|
113
165
|
end # lock
|
114
166
|
end # ExclusiveLocker
|
@@ -56,24 +56,90 @@ describe 'ZK::Client#locker' do
|
|
56
56
|
@zk.locker("my/multi/part/path").lock.should be_true
|
57
57
|
end
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
59
|
+
describe :with_lock do
|
60
|
+
# TODO: reorganize these tests so Convenience testing is done somewhere saner
|
61
|
+
#
|
62
|
+
# this tests ZK::Client::Conveniences, maybe shouldn't be *here*
|
63
|
+
describe 'Client::Conveniences' do
|
64
|
+
it %[should yield the lock instance to the block] do
|
65
|
+
@zk.with_lock(@path_to_lock) do |lock|
|
66
|
+
lock.should_not be_nil
|
67
|
+
lock.should be_kind_of(ZK::Locker::LockerBase)
|
68
|
+
lambda { lock.assert! }.should_not raise_error
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it %[should yield a shared lock when :mode => shared given] do
|
73
|
+
@zk.with_lock(@path_to_lock, :mode => :shared) do |lock|
|
74
|
+
lock.should_not be_nil
|
75
|
+
lock.should be_kind_of(ZK::Locker::SharedLocker)
|
76
|
+
lambda { lock.assert! }.should_not raise_error
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it %[should take a timeout] do
|
81
|
+
first_lock = @zk.locker(@path_to_lock)
|
82
|
+
first_lock.lock.should be_true
|
83
|
+
|
84
|
+
thread = Thread.new do
|
85
|
+
begin
|
86
|
+
@zk.with_lock(@path_to_lock, :wait => 0.01) do |lock|
|
87
|
+
raise "NO NO NO!! should not have called the block!!"
|
88
|
+
end
|
89
|
+
rescue Exception => e
|
90
|
+
@exc = e
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
thread.join(2).should == thread
|
95
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
68
96
|
end
|
69
|
-
array.length.should == 2
|
70
97
|
end
|
71
98
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
99
|
+
describe 'LockerBase' do
|
100
|
+
it "should blocking lock" do
|
101
|
+
array = []
|
102
|
+
first_lock = @zk.locker("mylock")
|
103
|
+
first_lock.lock.should be_true
|
104
|
+
array << :first_lock
|
105
|
+
|
106
|
+
thread = Thread.new do
|
107
|
+
@zk.locker("mylock").with_lock do
|
108
|
+
array << :second_lock
|
109
|
+
end
|
110
|
+
array.length.should == 2
|
111
|
+
end
|
112
|
+
|
113
|
+
array.length.should == 1
|
114
|
+
first_lock.unlock
|
115
|
+
thread.join(10)
|
116
|
+
array.length.should == 2
|
117
|
+
end
|
118
|
+
|
119
|
+
it %[should accept a :wait option] do
|
120
|
+
array = []
|
121
|
+
first_lock = @zk.locker("mylock")
|
122
|
+
first_lock.lock.should be_true
|
123
|
+
|
124
|
+
second_lock = @zk.locker("mylock")
|
125
|
+
|
126
|
+
thread = Thread.new do
|
127
|
+
begin
|
128
|
+
second_lock.with_lock(:wait => 0.01) do
|
129
|
+
array << :second_lock
|
130
|
+
end
|
131
|
+
rescue Exception => e
|
132
|
+
@exc = e
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
array.should be_empty
|
137
|
+
thread.join(2).should == thread
|
138
|
+
@exc.should_not be_nil
|
139
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end # with_lock
|
77
143
|
end
|
78
144
|
|
79
145
|
|
@@ -85,38 +85,93 @@ shared_examples_for 'ZK::Locker::SharedLocker' do
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
|
-
|
88
|
+
context do
|
89
89
|
before do
|
90
90
|
zk.mkdir_p(root_lock_path)
|
91
91
|
@write_lock_path = zk.create("#{root_lock_path}/#{ZK::Locker::EXCLUSIVE_LOCK_PREFIX}", '', :mode => :ephemeral_sequential)
|
92
|
-
|
92
|
+
@exc = nil
|
93
93
|
end
|
94
94
|
|
95
|
-
|
96
|
-
|
95
|
+
describe 'blocking success' do
|
96
|
+
it %[should acquire the lock after the write lock is released old-style] do
|
97
|
+
ary = []
|
97
98
|
|
98
|
-
|
99
|
+
locker.lock.should be_false
|
99
100
|
|
100
|
-
|
101
|
-
|
102
|
-
|
101
|
+
th = Thread.new do
|
102
|
+
locker.lock(true)
|
103
|
+
ary << :locked
|
104
|
+
end
|
105
|
+
|
106
|
+
locker.wait_until_blocked(5)
|
107
|
+
locker.should be_waiting
|
108
|
+
locker.should_not be_locked
|
109
|
+
ary.should be_empty
|
110
|
+
|
111
|
+
zk.delete(@write_lock_path)
|
112
|
+
|
113
|
+
th.join(2).should == th
|
114
|
+
|
115
|
+
ary.should_not be_empty
|
116
|
+
ary.length.should == 1
|
117
|
+
|
118
|
+
locker.should be_locked
|
103
119
|
end
|
104
120
|
|
105
|
-
|
106
|
-
|
107
|
-
locker.should_not be_locked
|
108
|
-
ary.should be_empty
|
121
|
+
it %[should acquire the lock after the write lock is released new-style] do
|
122
|
+
ary = []
|
109
123
|
|
110
|
-
|
124
|
+
locker.lock.should be_false
|
111
125
|
|
112
|
-
|
126
|
+
th = Thread.new do
|
127
|
+
locker.lock(:wait => true)
|
128
|
+
ary << :locked
|
129
|
+
end
|
113
130
|
|
114
|
-
|
115
|
-
|
131
|
+
locker.wait_until_blocked(5)
|
132
|
+
locker.should be_waiting
|
133
|
+
locker.should_not be_locked
|
134
|
+
ary.should be_empty
|
116
135
|
|
117
|
-
|
136
|
+
zk.delete(@write_lock_path)
|
137
|
+
|
138
|
+
th.join(2).should == th
|
139
|
+
|
140
|
+
ary.should_not be_empty
|
141
|
+
ary.length.should == 1
|
142
|
+
|
143
|
+
locker.should be_locked
|
144
|
+
end
|
118
145
|
end
|
119
|
-
|
146
|
+
|
147
|
+
describe 'blocking timeout' do
|
148
|
+
it %[should raise LockWaitTimeoutError] do
|
149
|
+
ary = []
|
150
|
+
|
151
|
+
locker.lock.should be_false
|
152
|
+
|
153
|
+
th = Thread.new do
|
154
|
+
begin
|
155
|
+
locker.lock(:wait => 0.01)
|
156
|
+
ary << :locked
|
157
|
+
rescue Exception => e
|
158
|
+
@exc = e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
locker.wait_until_blocked(5)
|
163
|
+
locker.should be_waiting
|
164
|
+
locker.should_not be_locked
|
165
|
+
ary.should be_empty
|
166
|
+
|
167
|
+
th.join(2).should == th
|
168
|
+
|
169
|
+
ary.should be_empty
|
170
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end # context
|
120
175
|
end # lock
|
121
176
|
|
122
177
|
it_should_behave_like 'LockerBase#unlock'
|
@@ -7,6 +7,7 @@ describe ZK::NodeDeletionWatcher do
|
|
7
7
|
@path = "#{@base_path}/node_deleteion_watcher_victim"
|
8
8
|
|
9
9
|
@n = ZK::NodeDeletionWatcher.new(@zk, @path)
|
10
|
+
@exc = nil
|
10
11
|
end
|
11
12
|
|
12
13
|
describe %[when the node already exists] do
|
@@ -31,8 +32,7 @@ describe ZK::NodeDeletionWatcher do
|
|
31
32
|
it %[should wake up if interrupt! is called] do
|
32
33
|
@zk.mkdir_p(@path)
|
33
34
|
|
34
|
-
|
35
|
-
|
35
|
+
# see _eric!! i had to do this because of 1.8.7!
|
36
36
|
th = Thread.new do
|
37
37
|
begin
|
38
38
|
@n.block_until_deleted
|
@@ -50,6 +50,28 @@ describe ZK::NodeDeletionWatcher do
|
|
50
50
|
|
51
51
|
@exc.should be_kind_of(ZK::Exceptions::WakeUpException)
|
52
52
|
end
|
53
|
+
|
54
|
+
it %[should raise LockWaitTimeoutError if we time out waiting for a node to be deleted] do
|
55
|
+
@zk.mkdir_p(@path)
|
56
|
+
|
57
|
+
th = Thread.new do
|
58
|
+
begin
|
59
|
+
@n.block_until_deleted(:timeout => 0.02)
|
60
|
+
rescue Exception => e
|
61
|
+
@exc = e
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@n.wait_until_blocked(5).should be_true
|
66
|
+
|
67
|
+
logger.debug { "wait_until_blocked returned" }
|
68
|
+
|
69
|
+
th.join(5).should == th
|
70
|
+
|
71
|
+
@exc.should be_kind_of(ZK::Exceptions::LockWaitTimeoutError)
|
72
|
+
@n.should be_done
|
73
|
+
@n.should be_timed_out
|
74
|
+
end
|
53
75
|
end
|
54
76
|
|
55
77
|
describe %[when the node doesn't exist] do
|
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: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 1.
|
8
|
+
- 7
|
9
|
+
- 0
|
10
|
+
version: 1.7.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: 2012-08-
|
19
|
+
date: 2012-08-29 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: zookeeper
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- lib/zk/install_fork_hooks.rb
|
100
100
|
- lib/zk/locker.rb
|
101
101
|
- lib/zk/locker/exclusive_locker.rb
|
102
|
+
- lib/zk/locker/lock_options.rb
|
102
103
|
- lib/zk/locker/locker_base.rb
|
103
104
|
- lib/zk/locker/shared_locker.rb
|
104
105
|
- lib/zk/logging.rb
|