zk 1.8.0 → 1.9.0

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