zk 1.5.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/Guardfile CHANGED
@@ -13,6 +13,11 @@ end
13
13
  guard 'rspec', :version => 2 do
14
14
  watch(%r{^spec/.+_spec\.rb$})
15
15
 
16
+ # run all specs when the support files change
17
+ watch(%r{^spec/support/.+\.rb$}) { 'spec' }
18
+
19
+ watch('spec/shared/client_examples.rb') { 'spec/zk/client_spec.rb' }
20
+
16
21
  watch(%r%^spec/support/client_forker.rb$%) { 'spec/zk/00_forked_client_integration_spec.rb' }
17
22
 
18
23
  watch(%r{^lib/(.+)\.rb$}) do |m|
@@ -23,10 +28,13 @@ guard 'rspec', :version => 2 do
23
28
  when 'zk/client/threaded'
24
29
  ["spec/zk/client_spec.rb", "spec/zk/zookeeper_spec.rb"]
25
30
 
31
+ when 'zk/locker'
32
+ 'spec/zk/locker_spec.rb'
33
+
26
34
  when %r{^(?:zk/locker/locker_base|spec/shared/locker)}
27
35
  Dir["spec/zk/locker/*_spec.rb"]
28
36
 
29
- when %r{^zk/client/(?:base|state_mixin)}
37
+ when %r{^zk/client/(?:base|state_mixin|unixisms)}
30
38
  Dir['spec/zk/{client,client/*,zookeeper}_spec.rb']
31
39
 
32
40
  when 'zk' # .rb
@@ -65,6 +65,38 @@ 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.0 ###
69
+
70
+ * Locker cleanup code!
71
+
72
+ 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:
73
+
74
+ ```ruby
75
+ ZK.open('localhost:2181') do |zk|
76
+ ZK::Locker.cleanup(zk)
77
+ end
78
+ ```
79
+
80
+ 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.
81
+
82
+ * 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.
83
+
84
+ * Added a bunch of shorter aliases on `ZK::Event`, so you can say `event.deleted?`, `event.changed?`, etc.
85
+
86
+ ### v1.5.3 ###
87
+
88
+ * 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.
89
+
90
+ * '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.
91
+
92
+ ### v1.5.2 ###
93
+
94
+ * Fix locker cleanup code to avoid a nasty race when a session is lost, see [issue #34](https://github.com/slyphon/zk/issues/34)
95
+
96
+ * Fix potential deadlock in ForkHook code so the mutex is unlocked in the case of an exception
97
+
98
+ * Do not hang forever when shutting down and the shutdown thread does not exit (wait 30 seconds).
99
+
68
100
  ### v1.5.1 ###
69
101
 
70
102
  * Added a `:retry_duration` option to the Threaded client constructor which will allows the user to specify for how long in the case of a connection loss, should an operation wait for the connection to be re-established before retrying the operation. This can be set at a global level and overridden on a per-call basis. The default is to not retry (which may change at a later date). Generally speaking, a timeout of > 30s is probably excessive, and care should be taken because during a connection loss, the server-side state may change without you being aware of it (i.e. events will not be delivered).
@@ -118,20 +150,6 @@ zk.delete('/some/path', :ignore => :no_node)
118
150
  * MASSIVE fork/parent/child test around event delivery and much greater stability expected for linux (with the zookeeper-1.0.3 gem). Again, please see the documentation on the wiki about [proper fork procedure](http://github.com/slyphon/zk/wiki/Forking).
119
151
 
120
152
 
121
- ### v1.3.1 ###
122
-
123
- * [fix a bug][bug 1.3.1] where a forked client would not have its 'outstanding watches' cleared, so some events would never be delivered
124
-
125
- [bug 1.3.1]: https://github.com/slyphon/zk/compare/release/1.3.0...9f68cee958fdaad8d32b6d042bf0a2c9ab5ec9b0
126
-
127
- ### v1.3.0 ###
128
-
129
- Phusion Passenger and Unicorn users are encouraged to upgrade!
130
-
131
- * __fork()__: ZK should now work reliably after a fork() if you call `reopen()` ASAP in the child process (before continuing any ZK work). Additionally, your event-handler (blocks set up with `zk.register`) will still work in the child. You will have to make calls like `zk.stat(path, :watch => true)` to tell ZooKeeper to notify you of events (as the child will have a new session), but everything should work.
132
-
133
- * See the fork-handling documentation [on the wiki](http://github.com/slyphon/zk/wiki/Forking).
134
-
135
153
 
136
154
  ## Caveats
137
155
 
@@ -1,5 +1,28 @@
1
1
  This file notes feature differences and bugfixes contained between releases.
2
2
 
3
+ ### v1.6.0 ###
4
+
5
+ * Locker cleanup code!
6
+
7
+ 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:
8
+
9
+ ```ruby
10
+ ZK.open('localhost:2181') do |zk|
11
+ ZK::Locker.cleanup(zk)
12
+ end
13
+ ```
14
+
15
+ 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.
16
+
17
+ * 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.
18
+
19
+ ### v1.5.3 ###
20
+
21
+ * 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.
22
+
23
+ * '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.
24
+
25
+
3
26
  ### v1.5.2 ###
4
27
 
5
28
  * Fix locker cleanup code to avoid a nasty race when a session is lost, see [issue #34](https://github.com/slyphon/zk/issues/34)
@@ -128,10 +128,6 @@ module ZK
128
128
  #
129
129
  # @return [Symbol] state of connection after operation
130
130
  def reopen(timeout=nil)
131
- # timeout ||= @session_timeout # XXX: @session_timeout ?
132
- # cnx.reopen(timeout)
133
- # @threadpool.start!
134
- # state
135
131
  end
136
132
 
137
133
  # close the underlying connection and clear all pending events.
@@ -202,6 +198,14 @@ module ZK
202
198
  # @option opts [Object] :context (nil) an object passed to the `:callback`
203
199
  # given as the `context` param
204
200
  #
201
+ # @option opts [:create,nil] :or (nil) syntactic sugar to say 'if this
202
+ # path already exists, then set its contents.' Note that this will
203
+ # also create all intermediate paths as it delegates to
204
+ # {ZK::Client::Unixisms#mkdir_p}. Note that this option can only be
205
+ # used to create or set persistent, non-sequential paths. If an
206
+ # option is used to specify either, an ArgumentError will be raised.
207
+ # (note: not available for zk-eventmachine)
208
+ #
205
209
  # @option opts [:ephemeral_sequential, :persistent_sequential, :persistent, :ephemeral] :mode (nil)
206
210
  # may be specified instead of :ephemeral and :sequence options. If `:mode` *and* either of
207
211
  # the `:ephermeral` or `:sequential` options are given, the `:mode` option will win
@@ -229,6 +233,14 @@ module ZK
229
233
  # @option opts [Object] :context (nil) an object passed to the `:callback`
230
234
  # given as the `context` param
231
235
  #
236
+ # @option opts [:create,nil] :or (nil) syntactic sugar to say 'if this
237
+ # path already exists, then set its contents.' Note that this will
238
+ # also create all intermediate paths as it delegates to
239
+ # {ZK::Client::Unixisms#mkdir_p}. Note that this option can only be
240
+ # used to create or set persistent, non-sequential paths. If an
241
+ # option is used to specify either, an ArgumentError will be raised.
242
+ # (note: not available for zk-eventmachine)
243
+ #
232
244
  # @option opts [:ephemeral_sequential, :persistent_sequential, :persistent, :ephemeral] :mode (nil)
233
245
  # may be specified instead of :ephemeral and :sequence options. If `:mode` *and* either of
234
246
  # the `:ephermeral` or `:sequential` options are given, the `:mode` option will win
@@ -287,7 +299,6 @@ module ZK
287
299
  # zk.create("/path", '', :mode => :persistent_sequential)
288
300
  # # => "/path0"
289
301
  #
290
- #
291
302
  # @example create ephemeral and sequential node
292
303
  #
293
304
  # zk.create("/path", '', :sequence => true, :ephemeral => true)
@@ -337,6 +348,14 @@ module ZK
337
348
  # zk.create("/path", "foo", :callback => callback, :context => context)
338
349
  #
339
350
  def create(path, *args)
351
+ h = parse_create_args(path, *args)
352
+ rv = call_and_check_rc(:create, h)
353
+ h[:callback] ? rv : rv[:path]
354
+ end
355
+
356
+ # parses the arguments and returns a hash for passing to
357
+ # call_and_check_rc. this is so subclasses can override easily
358
+ def parse_create_args(path, *args)
340
359
  opts = args.extract_options!
341
360
 
342
361
  # be somewhat strict about how many arguments we accept.
@@ -355,30 +374,29 @@ module ZK
355
374
 
356
375
  data = args.first || ''
357
376
 
358
- h = { :path => path, :data => data, :ephemeral => false, :sequence => false }.merge(opts)
377
+ rval = { :path => path, :data => data, :ephemeral => false, :sequence => false }.merge(opts)
359
378
 
360
- if mode = h.delete(:mode)
379
+ if mode = rval.delete(:mode)
361
380
  mode = mode.to_sym
362
381
 
363
382
  case mode
364
383
  when :ephemeral_sequential
365
- h[:ephemeral] = h[:sequence] = true
384
+ rval[:ephemeral] = rval[:sequence] = true
366
385
  when :persistent_sequential
367
- h[:ephemeral] = false
368
- h[:sequence] = true
386
+ rval[:ephemeral] = false
387
+ rval[:sequence] = true
369
388
  when :persistent
370
- h[:ephemeral] = false
389
+ rval[:ephemeral] = false
371
390
  when :ephemeral
372
- h[:ephemeral] = true
391
+ rval[:ephemeral] = true
373
392
  else
374
393
  raise ArgumentError, "Unknown mode: #{mode.inspect}"
375
394
  end
376
395
  end
377
396
 
378
- rv = call_and_check_rc(:create, h)
379
-
380
- h[:callback] ? rv : rv[:path]
397
+ rval
381
398
  end
399
+ private :parse_create_args
382
400
 
383
401
  # Return the data and stat of the node of the given path.
384
402
  #
@@ -48,6 +48,14 @@ module ZK
48
48
  end
49
49
  end
50
50
 
51
+ # Register a block to be called when *any* connection event occurs
52
+ #
53
+ # @yield [event] yields the connection event to the block
54
+ # @yieldparam event [ZK::Event] the event that occurred
55
+ def on_state_change(&block)
56
+ watcher.register_state_handler(:all, &block)
57
+ end
58
+
51
59
  # Register a block to be called on connection, when the client has
52
60
  # connected.
53
61
  #
@@ -347,7 +347,7 @@ module ZK
347
347
  on_tpool ? shutdown_thread : shutdown_thread.join(30)
348
348
  end
349
349
 
350
- # {see Base#close}
350
+ # {see ZK::Client::Base#close}
351
351
  def close
352
352
  super
353
353
  subs, @fork_subs = @fork_subs, []
@@ -365,6 +365,33 @@ module ZK
365
365
  @threadpool.on_exception(&blk)
366
366
  end
367
367
 
368
+ def closed?
369
+ return true if @mutex.synchronize { @client_state == CLOSED }
370
+ super
371
+ end
372
+
373
+ # this is where the :on option is implemented for {Base#create}
374
+ def create(path, *args)
375
+ opts = args.extract_options!
376
+
377
+ or_opt = opts.delete(:or)
378
+ args << opts
379
+
380
+ if or_opt
381
+ hash = parse_create_args(path, *args)
382
+
383
+ raise ArgumentError, "valid options for :or are nil or :set, not #{or_opt.inspect}" unless or_opt == :set
384
+ raise ArgumentError, "you cannot create an ephemeral node when using the :or option" if hash[:ephemeral]
385
+ raise ArgumentError, "you cannot create an sequence node when using the :or option" if hash[:sequence]
386
+
387
+ mkdir_p(path, :data => hash[:data])
388
+ path
389
+ else
390
+ # ok, none of our business, hand it up to mangement
391
+ super(path, *args)
392
+ end
393
+ end
394
+
368
395
  # @private
369
396
  def raw_event_handler(event)
370
397
  return unless event.session_event?
@@ -378,43 +405,47 @@ module ZK
378
405
  logger.error { "BUG: Exception caught in raw_event_handler: #{e.to_std_format}" }
379
406
  end
380
407
 
381
- def closed?
382
- return true if @mutex.synchronize { @client_state == CLOSED }
383
- super
384
- end
385
-
386
- # are we in running (not-paused) state?
387
- # @private
388
- def running?
389
- @mutex.synchronize { @client_state == RUNNING }
390
- end
391
-
392
- # are we in paused state?
393
- # @private
394
- def paused?
395
- @mutex.synchronize { @client_state == PAUSED }
396
- end
397
-
398
- # has shutdown time arrived?
399
- # @private
400
- def close_requested?
401
- @mutex.synchronize { @client_state == CLOSE_REQ }
402
- end
403
-
404
408
  # @private
405
409
  def wait_until_connected_or_dying(timeout)
406
410
  time_to_stop = Time.now + timeout
407
411
 
408
412
  @mutex.synchronize do
409
- while (@last_cnx_state != Zookeeper::ZOO_CONNECTED_STATE) && (Time.now < time_to_stop) && (@client_state == RUNNING)
410
- @cond.wait(timeout)
413
+ while true
414
+ now = Time.now
415
+ break if (@last_cnx_state == Zookeeper::ZOO_CONNECTED_STATE) || (now > time_to_stop) || (@client_state != RUNNING)
416
+ deadline = time_to_stop.to_f - now.to_f
417
+ @cond.wait(deadline)
411
418
  end
412
419
 
413
- logger.debug { "@last_cnx_state: #{@last_cnx_state.inspect}, time_left? #{Time.now.to_f < time_to_stop.to_f}, @client_state: #{@client_state.inspect}" }
420
+ logger.debug { "#{__method__} @last_cnx_state: #{@last_cnx_state.inspect}, time_left? #{Time.now.to_f < time_to_stop.to_f}, @client_state: #{@client_state.inspect}" }
414
421
  end
415
422
  end
416
423
 
424
+ # @private
425
+ def client_state
426
+ @mutex.synchronize { @client_state }
427
+ end
428
+
417
429
  private
430
+ # are we in running (not-paused) state?
431
+ def running?
432
+ @client_state == RUNNING
433
+ end
434
+
435
+ # are we in paused state?
436
+ def paused?
437
+ @client_state == PAUSED
438
+ end
439
+
440
+ # has shutdown time arrived?
441
+ def close_requested?
442
+ @client_state == CLOSE_REQ
443
+ end
444
+
445
+ def dead_or_dying?
446
+ (@client_state == CLOSE_REQ) || (@client_state == CLOSED)
447
+ end
448
+
418
449
  # this is just here so we can see it in stack traces
419
450
  def reopen_after_session_expired
420
451
  reopen
@@ -433,11 +464,11 @@ module ZK
433
464
  @mutex.synchronize do
434
465
  # either we havne't seen a valid session update from this
435
466
  # connection yet, or we're doing fine, so just wait
436
- @cond.wait_while { !seen_session_state_event? or (valid_session_state? and (@client_state == RUNNING)) }
467
+ @cond.wait_while { !seen_session_state_event? or (valid_session_state? and running?) }
437
468
 
438
469
  # we've entered into a non-running state, so we exit
439
470
  # note: need to restart this thread after a fork in parent
440
- if @client_state != RUNNING
471
+ unless running?
441
472
  logger.debug { "session failure watcher thread exiting, @client_state: #{@client_state}" }
442
473
  return
443
474
  end
@@ -457,8 +488,6 @@ module ZK
457
488
  end
458
489
  end
459
490
  end
460
- ensure
461
- logger.debug { "reconnect thread exiting" }
462
491
  end
463
492
 
464
493
  def join_and_clear_reconnect_thread
@@ -488,7 +517,7 @@ module ZK
488
517
 
489
518
  wait_until_connected_or_dying(retry_duration)
490
519
 
491
- if (@last_cnx_state != Zookeeper::ZOO_CONNECTED_STATE) || (Time.now > time_to_stop) || (@client_state != RUNNING)
520
+ if (@last_cnx_state != Zookeeper::ZOO_CONNECTED_STATE) || (Time.now > time_to_stop) || !running?
492
521
  raise e
493
522
  else
494
523
  retry
@@ -519,10 +548,6 @@ module ZK
519
548
  ::Zookeeper.new(*args)
520
549
  end
521
550
 
522
- def dead_or_dying?
523
- (@client_state == CLOSE_REQ) || (@client_state == CLOSED)
524
- end
525
-
526
551
  def unlocked_connect(opts={})
527
552
  return if @cnx
528
553
  timeout = opts.fetch(:timeout, @connection_timeout)
@@ -8,6 +8,8 @@ module ZK
8
8
  # zero data.
9
9
  #
10
10
  # @param [String] path An absolute znode path to create
11
+ #
12
+ # @option opts [String] :data ('') The data to place at path
11
13
  #
12
14
  # @example
13
15
  #
@@ -17,12 +19,20 @@ module ZK
17
19
  # zk.mkdir_p('/path/to/blah')
18
20
  # # => "/path/to/blah"
19
21
  #
20
- def mkdir_p(path)
21
- # TODO: write a non-recursive version of this. ruby doesn't have TCO, so
22
- # this could get expensive w/ psychotically long paths
22
+ def mkdir_p(path, opts={})
23
+ data = ''
24
+
25
+ # if we haven't recursed, or we recursed and now we're back at the top
26
+ if !opts.has_key?(:orig_path) or (path == opts[:orig_path])
27
+ data = opts.fetch(:data, '') # only put the data at the leaf node
28
+ end
23
29
 
24
- create(path, '', :mode => :persistent)
30
+ create(path, data, :mode => :persistent)
25
31
  rescue NodeExists
32
+ if !opts.has_key?(:orig_path) or (path == opts[:orig_path]) # we're at the leaf node
33
+ set(path, data)
34
+ end
35
+
26
36
  return
27
37
  rescue NoNode
28
38
  if File.dirname(path) == '/'
@@ -30,7 +40,9 @@ module ZK
30
40
  raise NonExistentRootError, "could not create '/', are you chrooted into a non-existent path?", caller
31
41
  end
32
42
 
33
- mkdir_p(File.dirname(path))
43
+ opts[:orig_path] ||= path
44
+
45
+ mkdir_p(File.dirname(path), opts)
34
46
  retry
35
47
  end
36
48
 
@@ -102,21 +102,25 @@ module ZK
102
102
  def node_created?
103
103
  @type == ZOO_CREATED_EVENT
104
104
  end
105
+ alias created? node_created?
105
106
 
106
107
  # Has a node been deleted?
107
108
  def node_deleted?
108
109
  @type == ZOO_DELETED_EVENT
109
110
  end
111
+ alias deleted? node_deleted?
110
112
 
111
113
  # Has a node changed?
112
114
  def node_changed?
113
115
  @type == ZOO_CHANGED_EVENT
114
116
  end
117
+ alias changed? node_changed?
115
118
 
116
119
  # Has a node's list of children changed?
117
120
  def node_child?
118
121
  @type == ZOO_CHILD_EVENT
119
122
  end
123
+ alias child? node_child?
120
124
 
121
125
  # Is this a session-related event?
122
126
  #
@@ -148,12 +152,14 @@ module ZK
148
152
  @type == ZOO_SESSION_EVENT
149
153
  end
150
154
  alias state_event? session_event?
155
+ alias session? session_event?
151
156
 
152
157
  # has this watcher been called because of a change to a zookeeper node?
153
158
  # `node_event?` and `session_event?` are mutually exclusive.
154
159
  def node_event?
155
160
  path and not path.empty?
156
161
  end
162
+ alias node? node_event?
157
163
 
158
164
  # according to [the programmer's guide](http://zookeeper.apache.org/doc/r3.3.4/zookeeperProgrammers.html#Java+Binding)
159
165
  #
@@ -14,6 +14,9 @@ module ZK
14
14
  # @private
15
15
  ALL_NODE_EVENTS_KEY = :all_node_events
16
16
 
17
+ # @private
18
+ ALL_STATE_EVENTS_KEY = :all_state_events
19
+
17
20
  # @private
18
21
  ZOOKEEPER_WATCH_TYPE_MAP = {
19
22
  Zookeeper::ZOO_CREATED_EVENT => :data,
@@ -100,7 +103,13 @@ module ZK
100
103
  # or when there's a temporary loss in connection and Zookeeper recommends
101
104
  # you go into 'safe mode'.
102
105
  #
103
- # @param [String] state The state you want to register for.
106
+ # Note that these callbacks are *not* one-shot like the path callbacks,
107
+ # these will be called back with every relative state event, there is
108
+ # no need to re-register
109
+ #
110
+ # @param [String,:all] state The state you want to register for or :all
111
+ # to be called back with every state change
112
+ #
104
113
  # @param [Block] block the block to execute on state changes
105
114
  # @yield [event] yields your block with
106
115
  #
@@ -156,8 +165,8 @@ module ZK
156
165
  cb_keys =
157
166
  if event.node_event?
158
167
  [event.path, ALL_NODE_EVENTS_KEY]
159
- elsif event.state_event?
160
- [state_key(event.state)]
168
+ elsif event.session_event?
169
+ [state_key(event.state), ALL_STATE_EVENTS_KEY]
161
170
  else
162
171
  raise ZKError, "don't know how to process event: #{event.inspect}"
163
172
  end
@@ -180,22 +189,21 @@ module ZK
180
189
  safe_call(cb_ary, event)
181
190
  end
182
191
 
183
- private
184
- # happens inside the lock, clears the restriction on setting new watches
185
- # for a given path/event type combination
186
- #
187
- def clear_watch_restrictions(event)
188
- return unless event.node_event?
189
-
190
- if watch_type = ZOOKEEPER_WATCH_TYPE_MAP[event.type]
191
- #logger.debug { "re-allowing #{watch_type.inspect} watches on path #{event.path.inspect}" }
192
-
193
- # we recieved a watch event for this path, now we allow code to set new watchers
194
- @outstanding_watches[watch_type].delete(event.path)
195
- end
192
+ # happens inside the lock, clears the restriction on setting new watches
193
+ # for a given path/event type combination
194
+ #
195
+ def clear_watch_restrictions(event)
196
+ return unless event.node_event?
197
+
198
+ if watch_type = ZOOKEEPER_WATCH_TYPE_MAP[event.type]
199
+ #logger.debug { "re-allowing #{watch_type.inspect} watches on path #{event.path.inspect}" }
200
+
201
+ # we recieved a watch event for this path, now we allow code to set new watchers
202
+ @outstanding_watches[watch_type].delete(event.path)
196
203
  end
204
+ end
205
+ private :clear_watch_restrictions
197
206
 
198
- public
199
207
  # used during shutdown to clear registered listeners
200
208
  # @private
201
209
  def clear! #:nodoc:
@@ -237,11 +245,6 @@ module ZK
237
245
  @callbacks.values.flatten.each(&:resume_after_fork_in_parent)
238
246
  end
239
247
 
240
- # @private
241
- def synchronize
242
- @mutex.synchronize { yield }
243
- end
244
-
245
248
  # @private
246
249
  def get_default_watcher_block
247
250
  @default_watcher_block ||= lambda do |hash|
@@ -303,6 +306,10 @@ module ZK
303
306
  end
304
307
 
305
308
  private
309
+ def synchronize
310
+ @mutex.synchronize { yield }
311
+ end
312
+
306
313
  def watcher_callback
307
314
  Zookeeper::Callbacks::WatcherCallback.create { |event| process(event) }
308
315
  end
@@ -310,12 +317,15 @@ module ZK
310
317
  def state_key(arg)
311
318
  int =
312
319
  case arg
320
+ when :all
321
+ # XXX: this is a nasty side-exit
322
+ return ALL_STATE_EVENTS_KEY
313
323
  when String, Symbol
314
324
  Zookeeper::Constants.const_get(:"ZOO_#{arg.to_s.upcase}_STATE")
315
325
  when Integer
316
326
  arg
317
327
  else
318
- raise NameError # ugh lame
328
+ raise NameError, "unrecognized state: #{arg.inspect}" # ugh lame
319
329
  end
320
330
 
321
331
  "state_#{int}"
@@ -99,24 +99,55 @@ module ZK
99
99
  # the default root path we will use when a value is not given to a
100
100
  # constructor
101
101
  attr_accessor :default_root_lock_node
102
- end
103
102
 
104
- # Create a {SharedLocker} instance
105
- #
106
- # @param client (see LockerBase#initialize)
107
- # @param name (see LockerBase#initialize)
108
- # @return [SharedLocker]
109
- def self.shared_locker(client, name, *args)
110
- SharedLocker.new(client, name, *args)
111
- end
103
+ # Create a {SharedLocker} instance
104
+ #
105
+ # @param client (see LockerBase#initialize)
106
+ # @param name (see LockerBase#initialize)
107
+ # @return [SharedLocker]
108
+ def shared_locker(client, name, *args)
109
+ SharedLocker.new(client, name, *args)
110
+ end
111
+
112
+ # Create an {ExclusiveLocker} instance
113
+ #
114
+ # @param client (see LockerBase#initialize)
115
+ # @param name (see LockerBase#initialize)
116
+ # @return [ExclusiveLocker]
117
+ def exclusive_locker(client, name, *args)
118
+ ExclusiveLocker.new(client, name, *args)
119
+ end
112
120
 
113
- # Create an {ExclusiveLocker} instance
114
- #
115
- # @param client (see LockerBase#initialize)
116
- # @param name (see LockerBase#initialize)
117
- # @return [ExclusiveLocker]
118
- def self.exclusive_locker(client, name, *args)
119
- ExclusiveLocker.new(client, name, *args)
121
+ # Clean up dead locker directories. There are situations (particularly
122
+ # session expiration) where a lock's directory will never be cleaned up.
123
+ #
124
+ # It is intened to be run periodically (perhaps from cron).
125
+ #
126
+ #
127
+ # This implementation goes through each lock directory and attempts to
128
+ # acquire an exclusive lock. If the lock is acquired then when it unlocks
129
+ # it will remove the locker directory. This is safe because the unlock
130
+ # code is designed to deal with the inherent race conditions.
131
+ #
132
+ # @example
133
+ #
134
+ # ZK.open do |zk|
135
+ # ZK::Locker.cleanup!(zk)
136
+ # end
137
+ #
138
+ # @param client [ZK::Client::Threaded] the client connection to use
139
+ #
140
+ # @param root_lock_node [String] if given, use an alternate root lock node to base
141
+ # each Locker's path on. You probably don't need to touch this. Uses
142
+ # {Locker.default_root_lock_node} by default (if value is nil)
143
+ #
144
+ def cleanup(client, root_lock_node=default_root_lock_node)
145
+ client.children(root_lock_node).each do |name|
146
+ exclusive_locker(client, name, root_lock_node).tap do |locker|
147
+ locker.unlock if locker.lock
148
+ end
149
+ end
150
+ end
120
151
  end
121
152
 
122
153
  # @private
@@ -75,16 +75,23 @@ module ZK
75
75
  path = "#{root_lock_path}/#{next_lowest_node}"
76
76
  logger.debug { "#{self.class}##{__method__} path=#{path.inspect}" }
77
77
 
78
- synchronize do
78
+ @mutex.synchronize do
79
+ logger.debug { "assigning the @node_deletion_watcher" }
79
80
  @node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
81
+ logger.debug { "broadcasting" }
80
82
  @cond.broadcast
81
83
  end
82
84
 
85
+ logger.debug { "calling block_until_deleted" }
86
+ Thread.pass
87
+
83
88
  @node_deletion_watcher.block_until_deleted
84
89
  rescue WeAreTheLowestLockNumberException
90
+ ensure
91
+ logger.debug { "block_until_deleted returned" }
85
92
  end
86
93
 
87
- synchronize { @locked = true }
94
+ @mutex.synchronize { @locked = true }
88
95
  end
89
96
  end # ExclusiveLocker
90
97
  end # Locker
@@ -129,11 +129,13 @@ module ZK
129
129
  #
130
130
  def unlock
131
131
  rval = false
132
- synchronize do
132
+ @mutex.synchronize do
133
133
  if @locked
134
+ logger.debug { "unlocking" }
134
135
  rval = cleanup_lock_path!
135
136
  @locked = false
136
137
  @node_deletion_watcher = nil
138
+ @cond.broadcast
137
139
  end
138
140
  end
139
141
  rval
@@ -176,7 +178,7 @@ module ZK
176
178
  #
177
179
  # @private
178
180
  def waiting?
179
- synchronize do
181
+ @mutex.synchronize do
180
182
  !!(@node_deletion_watcher and @node_deletion_watcher.blocked?)
181
183
  end
182
184
  end
@@ -184,9 +186,19 @@ module ZK
184
186
  # blocks the caller until this lock is blocked
185
187
  # @private
186
188
  def wait_until_blocked(timeout=nil)
187
- synchronize do
188
- @cond.wait_until { @node_deletion_watcher }
189
+ time_to_stop = timeout ? (Time.now + timeout) : nil
190
+
191
+ @mutex.synchronize do
192
+ if @node_deletion_watcher
193
+ logger.debug { "@node_deletion_watcher already assigned, not waiting" }
194
+ else
195
+ logger.debug { "going to wait up to #{timeout} sec for a @node_deletion_watcher to be assigned" }
196
+
197
+ @cond.wait(timeout)
198
+ raise "Timeout waiting for @node_deletion_watcher" unless @node_deletion_watcher
199
+ end
189
200
  end
201
+ logger.debug { "ok, @node_deletion_watcher: #{@node_deletion_watcher}, going to call wait_until_blocked" }
190
202
 
191
203
  @node_deletion_watcher.wait_until_blocked(timeout)
192
204
  end
@@ -220,7 +232,7 @@ module ZK
220
232
  # end
221
233
  #
222
234
  def assert!
223
- synchronize do
235
+ @mutex.synchronize do
224
236
  raise LockAssertionFailedError, "have not obtained the lock yet" unless locked?
225
237
  raise LockAssertionFailedError, "not connected" unless zk.connected?
226
238
  raise LockAssertionFailedError, "lock_path was #{lock_path.inspect}" unless lock_path
@@ -239,11 +251,6 @@ module ZK
239
251
  self.class.digit_from_lock_path(path)
240
252
  end
241
253
 
242
- # possibly lighter weight check to see if the lock path has any children
243
- # (using stat, rather than getting the list of children).
244
- def any_lock_children?
245
- end
246
-
247
254
  def lock_children(watch=false)
248
255
  zk.children(root_lock_path, :watch => watch)
249
256
  end
@@ -275,7 +282,7 @@ module ZK
275
282
  # [rule #34](https://github.com/slyphon/zk/issues/34)...er, *issue* #34.
276
283
  #
277
284
  def create_lock_path!(prefix='lock')
278
- synchronize do
285
+ @mutex.synchronize do
279
286
  @lock_path = @zk.create("#{root_lock_path}/#{prefix}", :mode => :ephemeral_sequential)
280
287
  @parent_stat = @zk.stat(root_lock_path)
281
288
  end
@@ -294,7 +301,7 @@ module ZK
294
301
  # see [issue #34](https://github.com/slyphon/zk/issues/34)
295
302
  #
296
303
  def root_lock_path_same?
297
- synchronize do
304
+ @mutex.synchronize do
298
305
  return false unless @parent_stat
299
306
 
300
307
  cur_stat = zk.stat(root_lock_path)
@@ -311,7 +318,7 @@ module ZK
311
318
  def cleanup_lock_path!
312
319
  rval = false
313
320
 
314
- synchronize do
321
+ @mutex.synchronize do
315
322
  if root_lock_path_same?
316
323
  logger.debug { "removing lock path #{@lock_path}" }
317
324
 
@@ -24,7 +24,7 @@ module ZK
24
24
  @mutex = Monitor.new # ffs, 1.8.7 compatibility w/ timeouts
25
25
  @cond = @mutex.new_cond
26
26
 
27
- @blocked = :not_yet
27
+ @blocked = NOT_YET
28
28
  @result = nil
29
29
  end
30
30
 
@@ -55,6 +55,7 @@ module ZK
55
55
  start = Time.now
56
56
  time_to_stop = timeout ? (start + timeout) : nil
57
57
 
58
+ logger.debug { "#{__method__} @blocked: #{@blocked.inspect} about to wait" }
58
59
  @cond.wait(timeout)
59
60
 
60
61
  if (time_to_stop and (Time.now > time_to_stop)) and (@blocked == NOT_YET)
@@ -100,27 +101,25 @@ module ZK
100
101
 
101
102
  logger.debug { "ok, going to block: #{path}" }
102
103
 
103
- while true # this is probably unnecessary
104
- @blocked = BLOCKED
105
- @cond.broadcast # wake threads waiting for @blocked to change
106
- @cond.wait_until { @result } # wait until we get a result
107
- @blocked = NOT_ANYMORE
104
+ @blocked = BLOCKED
105
+ @cond.broadcast # wake threads waiting for @blocked to change
106
+ @cond.wait_until { @result } # wait until we get a result
107
+ @blocked = NOT_ANYMORE
108
108
 
109
- case @result
110
- when :deleted
111
- logger.debug { "path #{path} was deleted" }
112
- return true
113
- when INTERRUPTED
114
- raise ZK::Exceptions::WakeUpException
115
- when ZOO_EXPIRED_SESSION_STATE
116
- raise Zookeeper::Exceptions::SessionExpired
117
- when ZOO_CONNECTING_STATE
118
- raise Zookeeper::Exceptions::NotConnected
119
- when ZOO_CLOSED_STATE
120
- raise Zookeeper::Exceptions::ConnectionClosed
121
- else
122
- raise "Hit unexpected case in block_until_node_deleted, result was: #{@result.inspect}"
123
- end
109
+ case @result
110
+ when :deleted
111
+ logger.debug { "path #{path} was deleted" }
112
+ return true
113
+ when INTERRUPTED
114
+ raise ZK::Exceptions::WakeUpException
115
+ when ZOO_EXPIRED_SESSION_STATE
116
+ raise Zookeeper::Exceptions::SessionExpired
117
+ when ZOO_CONNECTING_STATE
118
+ raise Zookeeper::Exceptions::NotConnected
119
+ when ZOO_CLOSED_STATE
120
+ raise Zookeeper::Exceptions::ConnectionClosed
121
+ else
122
+ raise "Hit unexpected case in block_until_node_deleted, result was: #{@result.inspect}"
124
123
  end
125
124
  end
126
125
  ensure
@@ -18,6 +18,7 @@ module ZK
18
18
 
19
19
  def initialize(parent, block)
20
20
  raise ArgumentError, "block must repsond_to?(:call)" unless block.respond_to?(:call)
21
+ raise ArgumentError, "parent must respond_to?(:unregister)" unless parent.respond_to?(:unregister)
21
22
  @parent = parent
22
23
  @callable = block
23
24
  @mutex = Monitor.new
@@ -1,3 +1,3 @@
1
1
  module ZK
2
- VERSION = "1.5.3"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -9,7 +9,6 @@ shared_context 'threaded client connection' do
9
9
 
10
10
  before do
11
11
  # logger.debug { "threaded client connection - begin before hook" }
12
-
13
12
  @connection_string = connection_host
14
13
  @base_path = '/zktests'
15
14
  @zk = ZK::Client::Threaded.new(*connection_args).tap { |z| wait_until { z.connected? } }
@@ -17,6 +16,9 @@ shared_context 'threaded client connection' do
17
16
  @zk.on_exception { |e| @threadpool_exception = e }
18
17
  @zk.rm_rf(@base_path)
19
18
 
19
+ @orig_default_root_lock_node = ZK::Locker.default_root_lock_node
20
+ ZK::Locker.default_root_lock_node = "#{@base_path}/_zklocking"
21
+
20
22
  # logger.debug { "threaded client connection - end before hook" }
21
23
  end
22
24
 
@@ -30,6 +32,8 @@ shared_context 'threaded client connection' do
30
32
  z.rm_rf(@base_path)
31
33
  end
32
34
 
35
+ ZK::Locker.default_root_lock_node = @orig_default_root_lock_node
36
+
33
37
  # logger.debug { "threaded client connection - end after hook" }
34
38
  end
35
39
  end
@@ -1,18 +1,35 @@
1
1
  shared_examples_for 'client' do
2
2
  describe :mkdir_p do
3
- before(:each) do
4
- @path_ary = %w[test mkdir_p path creation]
5
- @bogus_path = File.join('/', *@path_ary)
6
- @zk.rm_rf('/test')
3
+ before do
4
+ base = @base_path.sub(%r_^/_, '')
5
+
6
+ @path_ary = %W[#{base} test mkdir_p path creation]
7
+ @bogus_path = File.join('', *@path_ary)
7
8
  end
8
-
9
+
9
10
  it %[should create all intermediate paths for the path givem] do
10
- @zk.rm_rf('/test')
11
11
  @zk.should_not be_exists(@bogus_path)
12
12
  @zk.should_not be_exists(File.dirname(@bogus_path))
13
13
  @zk.mkdir_p(@bogus_path)
14
14
  @zk.should be_exists(@bogus_path)
15
15
  end
16
+
17
+ it %[should place the data only at the leaf node] do
18
+ @zk.mkdir_p(@bogus_path, :data => 'foobar')
19
+ @zk.get(@bogus_path).first.should == 'foobar'
20
+
21
+ path = ''
22
+ @path_ary[0..-2].each do |el|
23
+ path = File.join(path, el)
24
+ @zk.get(path).first.should == ''
25
+ end
26
+ end
27
+
28
+ it %[should replace the data at the leaf node if it already exists] do
29
+ @zk.mkdir_p(@bogus_path, :data => 'blahfoo')
30
+ @zk.mkdir_p(@bogus_path, :data => 'foodink')
31
+ @zk.get(@bogus_path).first.should == 'foodink'
32
+ end
16
33
  end
17
34
 
18
35
  # nail down all possible cases
@@ -114,6 +131,45 @@ shared_examples_for 'client' do
114
131
  proc { @zk.create("#{@base_path}/foo/bar/baz", :ignore => :no_node).should be_nil }.should_not raise_error(ZK::Exceptions::NoNode)
115
132
  end
116
133
  end
134
+
135
+ describe %[:or option] do
136
+ let(:path) { "#{@base_path}/foo/bar" }
137
+
138
+ it %[should barf if anything but the the :set value is given] do
139
+ proc { @zk.create(path, :or => :GFY) }.should raise_error(ArgumentError)
140
+ end
141
+
142
+ def create_args(opts={})
143
+ proc do
144
+ begin
145
+ @zk.create(path, opts.merge(:or => :set))
146
+ ensure
147
+ @zk.rm_rf(path)
148
+ end
149
+ end
150
+ end
151
+
152
+ it %[should barf if any node option besides 'persistent' is given] do
153
+ create_args(:persistent => true).should_not raise_error
154
+ create_args(:sequential => true).should raise_error(ArgumentError)
155
+ create_args(:mode => :ephemeral).should raise_error(ArgumentError)
156
+ create_args(:mode => :ephemeral_sequential).should raise_error(ArgumentError)
157
+ create_args(:mode => :sequential).should raise_error(ArgumentError)
158
+ end
159
+
160
+ it %[should replace the data at the leaf node if it already exists] do
161
+ @zk.mkdir_p(path, :data => 'foodink')
162
+ @zk.create(path, 'blahfoo', :or => :set)
163
+ @zk.get(path).first.should == 'blahfoo'
164
+ end
165
+
166
+ it %[should create the intermediate paths] do
167
+ proc { @zk.create(path, 'foobar', :or => :set) }.should_not raise_error
168
+
169
+ @zk.stat(@base_path).should exist
170
+ @zk.stat("#{@base_path}/foo").should exist
171
+ end
172
+ end
117
173
  end
118
174
 
119
175
  describe :delete do
@@ -298,20 +354,54 @@ shared_examples_for 'client' do
298
354
 
299
355
  describe 'reconnection' do
300
356
  it %[should if it receives a client_invalid? event] do
301
- logger.debug { "about to cause the reconnection" }
357
+ # note: we can't trust the events to be delivered in any particular order
358
+ # since they're happening on two different threads. if we see we're connected
359
+ # in the beginning, that there was a disconnection, then a reopen, that's
360
+ # probably fine.
361
+ #
362
+ # we also check that the session_id was changed, which is the desired effect
363
+
364
+ orig_session_id = @zk.session_id
365
+ @zk.should be_connected
302
366
 
303
- props = { :session_event? => true, :client_invalid? => true, :state_name => 'ZOO_EXPIRED_SESSION_STATE', :state => Zookeeper::ZOO_EXPIRED_SESSION_STATE }
367
+ props = {
368
+ :session_event? => true,
369
+ :node_event? => false,
370
+ :client_invalid? => true,
371
+ :state_name => 'ZOO_EXPIRED_SESSION_STATE',
372
+ :state => Zookeeper::ZOO_EXPIRED_SESSION_STATE,
373
+ }
304
374
 
305
375
  bogus_event = flexmock(:expired_session_event, props)
376
+ bogus_event.should_receive(:zk=).with(@zk).once
377
+
378
+ mutex = Monitor.new
379
+ cond = mutex.new_cond
380
+ events = []
306
381
 
307
- th = Thread.new do
308
- @zk.raw_event_handler(bogus_event)
382
+ @sub = @zk.on_state_change do |event|
383
+ mutex.synchronize do
384
+ logger.debug { "event: #{event.inspect}" }
385
+ events << event.state
386
+ cond.broadcast
387
+ end
309
388
  end
310
389
 
311
- @zk.wait_until_connected_or_dying(5)
312
- @zk.should be_connected
390
+ mutex.synchronize do
391
+ events.should be_empty
392
+ @zk.event_handler.process(bogus_event)
393
+ end
394
+
395
+ logger.debug { "events: #{events.inspect}" }
396
+
397
+ mutex.synchronize do
398
+ time_to_stop = Time.now + 2
399
+ cond.wait_while { (events.length < 2 ) && (Time.now < time_to_stop) }
400
+ end
313
401
 
314
- th.join(2).should == th
402
+ events.should include(Zookeeper::ZOO_EXPIRED_SESSION_STATE)
403
+ events.should include(Zookeeper::ZOO_CONNECTED_STATE)
404
+ @zk.session_id.should_not == orig_session_id
315
405
  end
316
406
  end # reconnection
317
407
 
@@ -2,10 +2,13 @@ module ZK
2
2
  TEST_LOG_PATH = File.join(ZK::ZK_ROOT, 'test.log')
3
3
 
4
4
  def self.logging_gem_setup
5
- layout = ::Logging.layouts.pattern(
5
+ layout_opts = {
6
6
  :pattern => '%.1l, [%d #%p] (%9.9T) %25.25c{2}: %m\n',
7
- :date_pattern => '%H:%M:%S.%6N'
8
- )
7
+ }
8
+
9
+ layout_opts[:date_pattern] = ZK.jruby? ? '%H:%M:%S.%3N' : '%H:%M:%S.%6N'
10
+
11
+ layout = ::Logging.layouts.pattern(layout_opts)
9
12
 
10
13
  appender = ENV['ZK_DEBUG'] ? ::Logging.appenders.stderr : ::Logging.appenders.file(ZK::TEST_LOG_PATH)
11
14
  appender.layout = layout
@@ -20,8 +20,11 @@ shared_examples_for 'ZK::Locker::ExclusiveLocker' do
20
20
  locker2.lock(true)
21
21
  end
22
22
 
23
+ th.run
24
+
23
25
  logger.debug { "calling wait_until_blocked" }
24
- locker2.wait_until_blocked(2)
26
+ proc { locker2.wait_until_blocked(2) }.should_not raise_error
27
+ logger.debug { "wait_until_blocked returned" }
25
28
  locker2.should be_waiting
26
29
 
27
30
  wait_until { zk.exists?(locker2.lock_path) }
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe ZK::Locker do
4
+ include_context 'threaded client connection'
5
+
6
+ describe :cleanup do
7
+ it %[should remove dead lock directories] do
8
+ locker = @zk.locker('legit')
9
+ locker.lock
10
+ locker.assert!
11
+
12
+ bogus_lock_dir_names = %w[a b c d e f g]
13
+ bogus_lock_dir_names.each { |n| @zk.create("#{ZK::Locker.default_root_lock_node}/#{n}") }
14
+
15
+ ZK::Locker.cleanup(@zk)
16
+
17
+ lambda { locker.assert! }.should_not raise_error
18
+
19
+ bogus_lock_dir_names.each { |n| @zk.stat("#{ZK::Locker.default_root_lock_node}/#{n}").should_not exist }
20
+
21
+ end
22
+ end
23
+ 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: 5
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
- - 5
9
- - 3
10
- version: 1.5.3
8
+ - 6
9
+ - 0
10
+ version: 1.6.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-05-24 00:00:00 Z
19
+ date: 2012-05-29 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: zookeeper
@@ -163,6 +163,7 @@ files:
163
163
  - spec/zk/locker/locker_basic_spec.rb
164
164
  - spec/zk/locker/shared_exclusive_integration_spec.rb
165
165
  - spec/zk/locker/shared_locker_spec.rb
166
+ - spec/zk/locker_spec.rb
166
167
  - spec/zk/module_spec.rb
167
168
  - spec/zk/mongoid_spec.rb
168
169
  - spec/zk/node_deletion_watcher_spec.rb
@@ -239,6 +240,7 @@ test_files:
239
240
  - spec/zk/locker/locker_basic_spec.rb
240
241
  - spec/zk/locker/shared_exclusive_integration_spec.rb
241
242
  - spec/zk/locker/shared_locker_spec.rb
243
+ - spec/zk/locker_spec.rb
242
244
  - spec/zk/module_spec.rb
243
245
  - spec/zk/mongoid_spec.rb
244
246
  - spec/zk/node_deletion_watcher_spec.rb