zk 1.6.5 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.6.4 ###
68
+ ### v1.7.0 ###
69
69
 
70
- * Remove unnecessary dependency on backports gem
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
- ### v1.6.2 ###
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
- * Change state call to reduce the chances of deadlocks
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
- One of the problems I've been seeing is that during some kind of shutdown event, some method will call `closed?` or `connected?` which will acquire a mutex and make a call on the underlying connection at the *exact* moment necessary to cause a deadlock. In order to help prevent this, and building on some changes from 1.5.3, we now treat our cached `@last_cnx_state` as the current state of the connection and don't touch the underlying connection object (except in the case of the java driver, which is safe).
76
+ ```ruby
77
+ zk = ZK.new
78
78
 
79
- ### v1.6.0 ###
79
+ locker = zk.locker('lock name')
80
80
 
81
- * Locker cleanup code!
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
- When a session is lost, it's likely that the locker's node name was left behind. so for `zk.locker('foo')` if the session is interrupted, it's very likely that the `/_zklocking/foo` znode has been left behind. A method has been added to allow you to safely clean up these stale znodes:
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
- Will go through your locker nodes one by one and try to lock and unlock them. If it succeeds, the lock is naturally cleaned up (as part of the normal teardown code), if it doesn't acquire the lock, then no harm, it knows that lock is still in use.
92
+ zk = ZK.new
92
93
 
93
- * Added `create('/path', 'data', :or => :set)` which will create a node (and all parent paths) with the given data or set its contents if it already exists. It's intended as a convenience when you just want a node to exist with a particular value.
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
- * Added a bunch of shorter aliases on `ZK::Event`, so you can say `event.deleted?`, `event.changed?`, etc.
102
+ ```
96
103
 
97
- ### v1.5.3 ###
104
+ ### v1.6.4 ###
98
105
 
99
- * Fixed reconnect code. There was an occasional race/deadlock condition caused because the reopen call was done on the underlying connection's dispatch thread. Closing the dispatch thread is part of reopen, so this would cause a deadlock in real-world use. Moved the reconnect logic to a separate, single-purpose thread on ZK::Client::Threaded that watches for connection state changes.
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
 
@@ -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
- mode = opts[:mode] || :exclusive
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
 
@@ -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
 
@@ -160,6 +160,7 @@ module ZK
160
160
  end # Locker
161
161
  end # ZK
162
162
 
163
+ require 'zk/locker/lock_options'
163
164
  require 'zk/locker/locker_base'
164
165
  require 'zk/locker/shared_locker'
165
166
  require 'zk/locker/exclusive_locker'
@@ -17,18 +17,8 @@ module ZK
17
17
  # (see LockerBase#lock)
18
18
  # obtain an exclusive lock.
19
19
  #
20
- def lock(blocking=false)
21
- return true if synchronize { @locked }
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
- def with_lock
69
- lock(true)
70
- yield
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
- # @param blocking [true,false] if true we block the caller until we can obtain
152
- # a lock on the resource
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
- # @see ZK::Client::Unixisms#block_until_node_deleted more about possible execptions
165
- def lock(blocking=false)
166
- raise NotImplementedError
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
- # (see #lock)
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!(blocking=false)
173
- lock(blocking)
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(blocking=false)
10
- return true if @locked
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
- # TODO: make this generic, can either block or non-block
97
- def block_until_read_lock!
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
- def block_until_deleted
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
- @cond.wait_until { @result } # wait until we get a result
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
- unless @result
159
- @result = event.state
160
- @cond.broadcast
161
- end
200
+ return if @result
201
+ @result = event.state
202
+ @cond.broadcast
162
203
  end
163
204
  end
164
205
  end
@@ -1,3 +1,3 @@
1
1
  module ZK
2
- VERSION = "1.6.5"
2
+ VERSION = "1.7.0"
3
3
  end
@@ -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
- it "should blocking lock" do
60
- array = []
61
- first_lock = @zk.locker("mylock")
62
- first_lock.lock.should be_true
63
- array << :first_lock
64
-
65
- thread = Thread.new do
66
- @zk.locker("mylock").with_lock do
67
- array << :second_lock
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
- array.length.should == 1
73
- first_lock.unlock
74
- thread.join(10)
75
- array.length.should == 2
76
- end
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
- describe 'blocking success' do
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
- $stderr.sync = true
92
+ @exc = nil
93
93
  end
94
94
 
95
- it %[should acquire the lock after the write lock is released] do
96
- ary = []
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
- locker.lock.should be_false
99
+ locker.lock.should be_false
99
100
 
100
- th = Thread.new do
101
- locker.lock(true)
102
- ary << :locked
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
- locker.wait_until_blocked(5)
106
- locker.should be_waiting
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
- zk.delete(@write_lock_path)
124
+ locker.lock.should be_false
111
125
 
112
- th.join(2).should == th
126
+ th = Thread.new do
127
+ locker.lock(:wait => true)
128
+ ary << :locked
129
+ end
113
130
 
114
- ary.should_not be_empty
115
- ary.length.should == 1
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
- locker.should be_locked
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
- end
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
- @exc = nil
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: 5
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
- - 6
9
- - 5
10
- version: 1.6.5
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-21 00:00:00 Z
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