zk 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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