zk 0.8.9 → 0.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.
@@ -1,5 +1,8 @@
1
1
  module ZK
2
2
  module Client
3
+ # EXTENSIONS
4
+ #
5
+ # convenience methods for dealing with zookeeper (rm -rf, mkdir -p, etc)
3
6
  module Conveniences
4
7
  # Queue an operation to be run on an internal threadpool. You may either
5
8
  # provide an object that responds_to?(:call) or pass a block. There is no
@@ -9,10 +12,13 @@ module ZK
9
12
  #
10
13
  # An ArgumentError will be raised if +callable+ does not <tt>respond_to?(:call)</tt>
11
14
  #
12
- # ==== Arguments
13
- # * <tt>callable</tt>: an object that <tt>respond_to?(:call)</tt>, takes precedence
14
- # over a given block
15
+ # @param [#call] callable an object that `respond_to?(:call)`, takes
16
+ # precedence over a given block
17
+ #
18
+ # @yield [] the block that should be run in the threadpool, if `callable`
19
+ # isn't given
15
20
  #
21
+ # @private
16
22
  def defer(callable=nil, &block)
17
23
  @threadpool.defer(callable, &block)
18
24
  end
@@ -28,18 +34,9 @@ module ZK
28
34
  false
29
35
  end
30
36
 
31
-
32
- #--
33
- #
34
- # EXTENSIONS
35
- #
36
- # convenience methods for dealing with zookeeper (rm -rf, mkdir -p, etc)
37
- #
38
- #++
39
-
40
37
  # creates a new locker based on the name you send in
41
38
  #
42
- # see ZK::Locker::ExclusiveLocker
39
+ # @see ZK::Locker::ExclusiveLocker
43
40
  #
44
41
  # returns a ZK::Locker::ExclusiveLocker instance using this Client and provided
45
42
  # lock name
@@ -0,0 +1,98 @@
1
+ module ZK
2
+ module Client
3
+ # A simple threadsafe way of having a thread deliver a single value
4
+ # to another thread.
5
+ #
6
+ # Each thread making requests will have a thread-local continuation
7
+ # that can be accessed via DropBox.current and one can use
8
+ # DropBox.with_current that will clear the result once the given block
9
+ # exits (allowing for reuse)
10
+ #
11
+ # (this class is in no way related to dropbox.com or Dropbox Inc.)
12
+ # @private
13
+ class DropBox
14
+ UNDEFINED = Object.new unless defined?(UNDEFINED)
15
+ KILL_TOKEN = Object.new unless defined?(KILL_TOKEN)
16
+ IMPOSSIBLE_TO_CONTINUE = Object.new unless defined?(IMPOSSIBLE_TO_CONTINUE)
17
+
18
+ # represents an exception to raise. if the pop method sees the value as an instance
19
+ # of this class, it will call the raise! method
20
+ class ExceptionValue
21
+ def initialize(exception_class, message)
22
+ @exception_class, @message = exception_class, message
23
+ end
24
+
25
+ def raise!
26
+ raise @exception_class, @message
27
+ end
28
+ end
29
+
30
+ THREAD_LOCAL_KEY = :__zk_client_continuation_current__ unless defined?(THREAD_LOCAL_KEY)
31
+
32
+ # @private
33
+ attr_reader :value
34
+
35
+ # sets the thread-local instance to nil, used by tests
36
+ # @private
37
+ def self.remove_current
38
+ Thread.current[THREAD_LOCAL_KEY] = nil
39
+ end
40
+
41
+ # access the thread-local DropBox instance for the current thread
42
+ def self.current
43
+ Thread.current[THREAD_LOCAL_KEY] ||= self.new()
44
+ end
45
+
46
+ # yields the current thread's DropBox instance and clears its value
47
+ # after the block returns
48
+ def self.with_current
49
+ yield current
50
+ ensure
51
+ current.clear
52
+ end
53
+
54
+ def initialize
55
+ @mutex = Mutex.new
56
+ @cond = ConditionVariable.new
57
+ @value = UNDEFINED # allows us to return nil
58
+ end
59
+
60
+ def push(obj)
61
+ @mutex.synchronize do
62
+ @value = obj
63
+ @cond.signal
64
+ end
65
+ end
66
+
67
+ def pop
68
+ @mutex.synchronize do
69
+ @cond.wait(@mutex)
70
+ @value.kind_of?(ExceptionValue) ? @value.raise! : @value
71
+ end
72
+ end
73
+
74
+ def clear
75
+ @mutex.synchronize do
76
+ @value = UNDEFINED
77
+ end
78
+ end
79
+
80
+ # we are done if value is defined, use clear to reset
81
+ def done?
82
+ @value != UNDEFINED
83
+ end
84
+
85
+ # called when you need the waiting thread to receive an exception
86
+ # returns nil if a value has already been set
87
+ def oh_noes!(exception_class, message)
88
+ @mutex.synchronize do
89
+ return if done?
90
+ @value = ExceptionValue.new(exception_class, message)
91
+ @cond.signal
92
+ end
93
+ true
94
+ end
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,28 @@
1
+ module ZK
2
+ module Client
3
+ # This client is an experimental implementation of a threaded and
4
+ # multiplexed client. The idea is that each synchronous request represents
5
+ # a continuation. This way, you can have multiple requests pending with the
6
+ # server simultaneously, and the responses will be delivered on the event
7
+ # thread (but run in the calling thread). This allows for higher throughput
8
+ # for multi-threaded applications.
9
+ #
10
+ # Asynchronous requests are not supported through this client.
11
+ #
12
+ class Multiplexed < Threaded
13
+ def close!
14
+ @cnx.connection_closed!
15
+ super
16
+ end
17
+
18
+ protected
19
+ def create_connection(*args)
20
+ ContinuationProxy.new.tap do |cp|
21
+ on_expired_session { cp.expired_session! } # hook up client's session expired event listener
22
+ cp.zookeeper_cnx = super(*args)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -2,35 +2,65 @@ module ZK
2
2
  module Client
3
3
  # This is the default client that ZK will use. In the zk-eventmachine gem,
4
4
  # there is an Evented client.
5
+ #
5
6
  class Threaded < Base
6
7
  include StateMixin
7
8
  include Unixisms
8
9
  include Conveniences
9
10
 
10
- # Create a new client and connect to the zookeeper server.
11
+ DEFAULT_THREADPOOL_SIZE = 1
12
+
13
+ # @note The `:timeout` argument here is *not* the session_timeout for the
14
+ # connection. rather it is the amount of time we wait for the connection
15
+ # to be established. The session timeout exchanged with the server is
16
+ # set to 10s by default in the C implemenation, and as of version 0.8.0
17
+ # of slyphon-zookeeper has yet to be exposed as an option. That feature
18
+ # is planned.
19
+ #
20
+ # @param [String] host (see ZK::Client::Base#initialize)
21
+ #
22
+ # @option opts [Fixnum] :threadpool_size the size of the threadpool that
23
+ # should be used to deliver events. In ZK 0.8.x this was set to 5, which
24
+ # means that events could be delivered concurrently. As of 0.9, this will
25
+ # be set to 1, so it's very important to _not block the event thread_.
11
26
  #
12
- # +host+ should be a string of comma-separated host:port pairs. You can
13
- # also supply an optional "chroot" suffix that will act as an implicit
14
- # prefix to all paths supplied.
27
+ # @option opts [Fixnum] :timeout how long we will wait for the connection
28
+ # to be established.
15
29
  #
16
- # example:
17
- #
18
- # ZK::Client.new("zk01:2181,zk02:2181/chroot/path")
30
+ # @yield [self] calls the block with the new instance after the event
31
+ # handler has been set up, but before any connections have been made.
32
+ # This allows the client to register watchers for session events like
33
+ # `connected`. You *cannot* perform any other operations with the client
34
+ # as you will get a NoMethodError (the underlying connection is nil).
19
35
  #
20
- def initialize(host, opts={})
21
- @event_handler = EventHandler.new(self)
36
+ def initialize(host, opts={}, &b)
37
+ super(host, opts)
38
+
39
+ @session_timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT) # maybe move this into superclass?
40
+ @event_handler = EventHandler.new(self)
41
+
22
42
  yield self if block_given?
23
- @cnx = ::Zookeeper.new(host, DEFAULT_TIMEOUT, @event_handler.get_default_watcher_block)
24
- @threadpool = Threadpool.new
43
+
44
+ @cnx = create_connection(host, @session_timeout, @event_handler.get_default_watcher_block)
45
+
46
+ tp_size = opts.fetch(:threadpool_size, DEFAULT_THREADPOOL_SIZE)
47
+
48
+ @threadpool = Threadpool.new(tp_size)
25
49
  end
26
50
 
27
- # closes the underlying connection and deregisters all callbacks
51
+ # @see ZK::Client::Base#close!
28
52
  def close!
29
53
  @threadpool.shutdown
30
54
  super
31
55
  nil
32
56
  end
57
+
58
+ protected
59
+ # allows for the Mutliplexed client to wrap the connection in its ContinuationProxy
60
+ # @private
61
+ def create_connection(*args)
62
+ ::Zookeeper.new(*args)
63
+ end
33
64
  end
34
65
  end
35
66
  end
36
-
@@ -5,11 +5,10 @@ module ZK
5
5
 
6
6
  # Creates all parent paths and 'path' in zookeeper as persistent nodes with
7
7
  # zero data.
8
- #
9
- # ==== Arguments
10
- # * <tt>path</tt>: An absolute znode path to create
11
- #
12
- # ==== Examples
8
+ #
9
+ # @param [String] path An absolute znode path to create
10
+ #
11
+ # @example
13
12
  #
14
13
  # zk.exists?('/path')
15
14
  # # => false
@@ -17,10 +16,10 @@ module ZK
17
16
  # zk.mkdir_p('/path/to/blah')
18
17
  # # => "/path/to/blah"
19
18
  #
20
- #--
21
- # TODO: write a non-recursive version of this. ruby doesn't have TCO, so
22
- # this could get expensive w/ psychotically long paths
23
19
  def mkdir_p(path)
20
+ # TODO: write a non-recursive version of this. ruby doesn't have TCO, so
21
+ # this could get expensive w/ psychotically long paths
22
+
24
23
  create(path, '', :mode => :persistent)
25
24
  rescue Exceptions::NodeExists
26
25
  return
@@ -49,23 +48,84 @@ module ZK
49
48
  end
50
49
  end
51
50
 
52
- # see ZK::Find for explanation
51
+ # Acts in a similar way to ruby's Find class. Performs a depth-first
52
+ # traversal of every node under the given paths, and calls the given
53
+ # block with each path found. Like the ruby Find class, you can call
54
+ # {ZK::Find.prune} to avoid descending further into a given sub-tree
55
+ #
56
+ # @example list the paths under a given node
57
+ #
58
+ # zk = ZK.new
59
+ #
60
+ # paths = %w[
61
+ # /root
62
+ # /root/alpha
63
+ # /root/bravo
64
+ # /root/charlie
65
+ # /root/charlie/rose
66
+ # /root/charlie/manson
67
+ # /root/charlie/manson/family
68
+ # /root/charlie/manson/murders
69
+ # /root/charlie/brown
70
+ # /root/delta
71
+ # /root/delta/blues
72
+ # /root/delta/force
73
+ # /root/delta/burke
74
+ # ]
75
+ #
76
+ # paths.each { |p| zk.create(p) }
77
+ #
78
+ # zk.find('/root') do |path|
79
+ # puts path
80
+ #
81
+ # ZK::Find.prune if path == '/root/charlie/manson'
82
+ # end
83
+ #
84
+ # # this produces the output:
85
+ #
86
+ # # /root
87
+ # # /root/alpha
88
+ # # /root/bravo
89
+ # # /root/charlie
90
+ # # /root/charlie/brown
91
+ # # /root/charlie/manson
92
+ # # /root/charlie/rose
93
+ # # /root/delta
94
+ # # /root/delta/blues
95
+ # # /root/delta/burke
96
+ # # /root/delta/force
97
+ #
98
+ # @param [Array[String]] paths a list of paths to recursively
99
+ # yield the sub-paths of
100
+ #
101
+ # @see ZK::Find#find
53
102
  def find(*paths, &block)
54
103
  ZK::Find.find(self, *paths, &block)
55
104
  end
56
105
 
57
- # will block the caller until +abs_node_path+ has been removed
58
- #
59
- # @private this method is of dubious value and may be removed in a later
60
- # version
106
+ # Will _safely_ block the caller until `abs_node_path` has been removed.
107
+ # This is trickier than it first appears. This method will wake the caller
108
+ # if a session event occurs that would ensure the event would never be
109
+ # delivered, and also checks to make sure that the caller is not calling
110
+ # from the event distribution thread (which would cause a deadlock).
61
111
  #
62
112
  # @note this is dangerous to use in callbacks! there is only one
63
113
  # event-delivery thread, so if you use this method in a callback or
64
114
  # watcher, you *will* deadlock!
115
+ #
116
+ # @raise [Exceptions::InterruptedSession] If a session event occurs while we're
117
+ # blocked waiting for the node to be deleted, an exception that
118
+ # mixes in the InterruptedSession module will be raised.
119
+ #
65
120
  def block_until_node_deleted(abs_node_path)
66
- queue = Queue.new
67
121
  subs = []
68
122
 
123
+ assert_we_are_not_on_the_event_dispatch_thread!
124
+
125
+ raise ArgumentError, "argument must be String-ish, not: #{abs_node_path.inspect}" unless abs_node_path
126
+
127
+ queue = Queue.new
128
+
69
129
  node_deletion_cb = lambda do |event|
70
130
  if event.node_deleted?
71
131
  queue.enq(:deleted)
data/lib/z_k/client.rb CHANGED
@@ -24,9 +24,12 @@ module ZK
24
24
  end
25
25
  end
26
26
 
27
+ require 'z_k/client/drop_box'
27
28
  require 'z_k/client/state_mixin'
28
29
  require 'z_k/client/unixisms'
29
30
  require 'z_k/client/conveniences'
30
31
  require 'z_k/client/base'
31
32
  require 'z_k/client/threaded'
33
+ require 'z_k/client/continuation_proxy'
34
+ require 'z_k/client/multiplexed'
32
35
 
@@ -32,51 +32,7 @@ module ZK
32
32
  end
33
33
  end
34
34
 
35
- # register a path with the handler
36
- #
37
- # your block will be called with all events on that path.
38
- #
39
- # @note All watchers are one-shot handlers. After an event is delivered to
40
- # your handler, you *must* re-watch the node to receive more events. This
41
- # leads to a pattern you will find throughout ZK code that avoids races,
42
- # see the example below "avoiding a race"
43
- #
44
- # @example avoiding a race waiting for a node to be deleted
45
- #
46
- # # we expect that '/path/to/node' exists currently and want to be notified
47
- # # when it's deleted
48
- #
49
- # # register a handler that will be called back when an event occurs on
50
- # # node
51
- # #
52
- # node_subscription = zk.event_handler.register('/path/to/node') do |event|
53
- # if event.node_deleted?
54
- # do_something_when_node_deleted
55
- # end
56
- # end
57
- #
58
- # # check to see if our condition is true *while* setting a watch on the node
59
- # # if our condition happens to be true while setting the watch
60
- # #
61
- # unless exists?('/path/to/node', :watch => true)
62
- # node_subscription.unsubscribe # cancel the watch
63
- # do_something_when_node_deleted # call the callback
64
- # end
65
- #
66
- #
67
- # @param [String] path the path you want to listen to
68
- #
69
- # @param [Block] block the block to execute when a watch event happpens
70
- #
71
- # @yield [event] We will call your block with the watch event object (which
72
- # has the connection the event occurred on as its #zk attribute)
73
- #
74
- # @return [ZooKeeper::EventHandlerSubscription] the subscription object
75
- # you can use to to unsubscribe from an event
76
- #
77
- # @see ZooKeeper::WatcherEvent
78
- # @see ZK::EventHandlerSubscription
79
- #
35
+ # @see ZK::Client::Base#register
80
36
  def register(path, &block)
81
37
  # logger.debug { "EventHandler#register path=#{path.inspect}" }
82
38
  EventHandlerSubscription.new(self, path, block).tap do |subscription|
@@ -136,6 +92,10 @@ module ZK
136
92
  alias :unsubscribe :unregister
137
93
 
138
94
  # called from the client-registered callback when an event fires
95
+ #
96
+ # @note this is *ONLY* dealing with asynchronous callbacks! watchers
97
+ # and session events go through here, NOT anything else!!
98
+ #
139
99
  # @private
140
100
  def process(event)
141
101
  # logger.debug { "EventHandler#process dispatching event: #{event.inspect}" }# unless event.type == -1
@@ -1,29 +1,36 @@
1
1
  module ZK
2
2
  # the subscription object that is passed back from subscribing
3
3
  # to events.
4
- # @see ZooKeeperEventHandler#subscribe
4
+ # @see ZK::Client::Base#register
5
5
  class EventHandlerSubscription
6
- attr_accessor :event_handler, :path, :callback
6
+ # the event handler associated with this subscription
7
+ # @return [EventHandler]
8
+ attr_accessor :event_handler
9
+
10
+ # the path this subscription is for
11
+ # @return [String]
12
+ attr_accessor :path
13
+
14
+ # the block associated with the path
15
+ # @return [Proc]
16
+ attr_accessor :callback
7
17
 
8
18
  # @private
9
- # :nodoc:
10
19
  def initialize(event_handler, path, callback)
11
20
  @event_handler, @path, @callback = event_handler, path, callback
12
21
  end
13
22
 
14
23
  # unsubscribe from the path or state you were watching
15
- # @see ZooKeeperEventHandler#subscribe
24
+ # @see ZK::Client::Base#register
16
25
  def unsubscribe
17
26
  @event_handler.unregister(self)
18
27
  end
19
28
  alias :unregister :unsubscribe
20
29
 
21
30
  # @private
22
- # :nodoc:
23
31
  def call(event)
24
32
  callback.call(event)
25
33
  end
26
-
27
34
  end
28
35
  end
29
36
 
@@ -35,10 +35,15 @@ module ZK
35
35
  end
36
36
  end
37
37
 
38
+ # This module is mixed into the session-related exceptions to allow
39
+ # one to rescue that group of exceptions. It is also mixed into the related
40
+ # ZookeeperException objects
41
+ module InterruptedSession
42
+ end
43
+
38
44
  class SystemError < KeeperException; end
39
45
  class RunTimeInconsistency < KeeperException; end
40
46
  class DataInconsistency < KeeperException; end
41
- class ConnectionLoss < KeeperException; end
42
47
  class MarshallingError < KeeperException; end
43
48
  class Unimplemented < KeeperException; end
44
49
  class OperationTimeOut < KeeperException; end
@@ -50,11 +55,23 @@ module ZK
50
55
  class NoChildrenForEphemerals < KeeperException; end
51
56
  class NodeExists < KeeperException; end
52
57
  class NotEmpty < KeeperException; end
53
- class SessionExpired < KeeperException; end
54
58
  class InvalidCallback < KeeperException; end
55
59
  class InvalidACL < KeeperException; end
56
60
  class AuthFailed < KeeperException; end
57
61
 
62
+ class ConnectionLoss < KeeperException
63
+ include InterruptedSession
64
+ end
65
+
66
+ class SessionExpired < KeeperException
67
+ include InterruptedSession
68
+ end
69
+
70
+ # mixes in InterruptedSession, and can be raised on its own
71
+ class InterruptedSessionException < KeeperException
72
+ include InterruptedSession
73
+ end
74
+
58
75
  ERROR_MAP = {
59
76
  SYSTEMERROR => SystemError,
60
77
  RUNTIMEINCONSISTENCY => RunTimeInconsistency,
@@ -100,6 +117,9 @@ module ZK
100
117
  # raised for certain operations when using a chrooted connection, but the
101
118
  # root doesn't exist.
102
119
  class NonExistentRootError < ZKError; end
120
+
121
+ # raised when someone performs a blocking ZK operation on the event dispatch thread.
122
+ class EventDispatchThreadException < ZKError; end
103
123
  end
104
124
  end
105
125
 
@@ -17,7 +17,7 @@ module ZK
17
17
  #
18
18
  # *args, if given, will be passed on *after* the callback
19
19
  #
20
- # example:
20
+ # @example
21
21
  #
22
22
  # WatcherCallback.create do |cb|
23
23
  # puts "watcher callback called with argument: #{cb.inspect}"
@@ -116,7 +116,6 @@ module ZK
116
116
  MEMBERS.all? { |m| self.__send__(m) == other.__send__(m) }
117
117
  end
118
118
  end
119
-
120
119
  end # Extensions
121
120
  end # ZK
122
121
 
@@ -125,6 +124,15 @@ ZookeeperCallbacks::Callback.send(:include, ZK::Extensions::Callbacks::Callback)
125
124
  ZookeeperCallbacks::WatcherCallback.send(:include, ZK::Extensions::Callbacks::WatcherCallbackExt)
126
125
  ZookeeperStat::Stat.send(:include, ZK::Extensions::Stat)
127
126
 
127
+ # Include the InterruptedSession module in key ZookeeperExceptions to allow
128
+ # clients to catch a single error type when waiting on a node (for example)
129
+
130
+ [:ConnectionClosed, :NotConnected, :SessionExpired, :SessionMoved, :ConnectionLoss].each do |class_name|
131
+ ZookeeperExceptions::ZookeeperException.const_get(class_name).tap do |klass|
132
+ klass.__send__(:include, ZK::Exceptions::InterruptedSession)
133
+ end
134
+ end
135
+
128
136
  class ::Exception
129
137
  unless method_defined?(:to_std_format)
130
138
  def to_std_format
data/lib/z_k/find.rb CHANGED
@@ -3,7 +3,10 @@ module ZK
3
3
  # like ruby's Find module, will call the given block with each _absolute_ znode path
4
4
  # under +paths+. you can call ZK::Find.prune if you want to not recurse
5
5
  # deeper under the current directory path.
6
- def find(zk, *paths) #:yield: znode_path
6
+ #
7
+ # @yield [String] each znode path under the list of paths given.
8
+ #
9
+ def find(zk, *paths)
7
10
  paths.collect!{|d| d.dup}
8
11
 
9
12
  while p = paths.shift
@@ -11,7 +14,7 @@ module ZK
11
14
  yield p.dup.taint
12
15
  next unless zk.exists?(p)
13
16
 
14
- zk.children(p).each do |ch|
17
+ zk.children(p).sort.reverse.each do |ch|
15
18
  paths.unshift ZK.join(p, ch).untaint
16
19
  end
17
20
  end
data/lib/z_k/locker.rb CHANGED
@@ -28,16 +28,21 @@ module ZK
28
28
  class LockerBase
29
29
  include ZK::Logging
30
30
 
31
- attr_accessor :zk #:nodoc:
31
+ # @private
32
+ attr_accessor :zk
32
33
 
33
34
  # our absolute lock node path
34
35
  #
35
36
  # ex. '/_zklocking/foobar/__blah/lock000000007'
36
- attr_reader :lock_path #;nodoc:
37
+ #
38
+ # @private
39
+ attr_reader :lock_path
37
40
 
38
- attr_reader :root_lock_path #:nodoc:
41
+ # @private
42
+ attr_reader :root_lock_path
39
43
 
40
- def self.digit_from_lock_path(path) #:nodoc:
44
+ # @private
45
+ def self.digit_from_lock_path(path)
41
46
  path[/0*(\d+)$/, 1].to_i
42
47
  end
43
48