zk 0.8.9 → 0.9.0

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