zk 0.9.1 → 1.0.0.rc.1

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.
Files changed (63) hide show
  1. data/.gitignore +2 -2
  2. data/Gemfile +3 -4
  3. data/README.markdown +14 -5
  4. data/RELEASES.markdown +69 -1
  5. data/Rakefile +50 -18
  6. data/docs/examples/block_until_node_deleted_ex.rb +63 -0
  7. data/docs/examples/events_01.rb +36 -0
  8. data/docs/examples/events_02.rb +41 -0
  9. data/lib/{z_k → zk}/client/base.rb +87 -30
  10. data/lib/{z_k → zk}/client/conveniences.rb +0 -1
  11. data/lib/{z_k → zk}/client/state_mixin.rb +0 -0
  12. data/lib/zk/client/threaded.rb +196 -0
  13. data/lib/{z_k → zk}/client/unixisms.rb +0 -0
  14. data/lib/zk/client.rb +59 -0
  15. data/lib/zk/core_ext.rb +75 -0
  16. data/lib/{z_k → zk}/election.rb +0 -0
  17. data/lib/zk/event.rb +168 -0
  18. data/lib/{z_k → zk}/event_handler.rb +53 -28
  19. data/lib/zk/event_handler_subscription.rb +68 -0
  20. data/lib/{z_k → zk}/exceptions.rb +38 -23
  21. data/lib/zk/extensions.rb +79 -0
  22. data/lib/{z_k → zk}/find.rb +0 -0
  23. data/lib/{z_k → zk}/locker.rb +0 -0
  24. data/lib/{z_k → zk}/logging.rb +0 -0
  25. data/lib/{z_k → zk}/message_queue.rb +8 -4
  26. data/lib/{z_k → zk}/mongoid.rb +0 -0
  27. data/lib/{z_k → zk}/pool.rb +0 -0
  28. data/lib/zk/stat.rb +115 -0
  29. data/lib/{z_k → zk}/threadpool.rb +52 -4
  30. data/lib/zk/version.rb +3 -0
  31. data/lib/zk.rb +238 -1
  32. data/spec/message_queue_spec.rb +2 -2
  33. data/spec/shared/client_contexts.rb +8 -20
  34. data/spec/shared/client_examples.rb +136 -2
  35. data/spec/spec_helper.rb +4 -2
  36. data/spec/support/event_catcher.rb +11 -0
  37. data/spec/support/exist_matcher.rb +6 -0
  38. data/spec/support/logging.rb +2 -1
  39. data/spec/watch_spec.rb +194 -10
  40. data/spec/{z_k → zk}/client/locking_and_session_death_spec.rb +0 -32
  41. data/spec/zk/client_spec.rb +23 -0
  42. data/spec/{z_k → zk}/election_spec.rb +0 -0
  43. data/spec/{z_k → zk}/extensions_spec.rb +0 -0
  44. data/spec/{z_k → zk}/locker_spec.rb +0 -40
  45. data/spec/zk/module_spec.rb +185 -0
  46. data/spec/{z_k → zk}/mongoid_spec.rb +0 -2
  47. data/spec/{z_k → zk}/pool_spec.rb +0 -2
  48. data/spec/{z_k → zk}/threadpool_spec.rb +32 -4
  49. data/spec/zookeeper_spec.rb +1 -6
  50. data/zk.gemspec +2 -2
  51. metadata +64 -56
  52. data/lib/z_k/client/continuation_proxy.rb +0 -109
  53. data/lib/z_k/client/drop_box.rb +0 -98
  54. data/lib/z_k/client/multiplexed.rb +0 -28
  55. data/lib/z_k/client/threaded.rb +0 -76
  56. data/lib/z_k/client.rb +0 -35
  57. data/lib/z_k/event_handler_subscription.rb +0 -36
  58. data/lib/z_k/extensions.rb +0 -155
  59. data/lib/z_k/version.rb +0 -3
  60. data/lib/z_k.rb +0 -97
  61. data/spec/z_k/client/drop_box_spec.rb +0 -90
  62. data/spec/z_k/client/multiplexed_spec.rb +0 -20
  63. data/spec/z_k/client_spec.rb +0 -7
@@ -0,0 +1,196 @@
1
+ module ZK
2
+ module Client
3
+ # This is the default client that ZK will use. In the zk-eventmachine gem,
4
+ # there is an Evented client.
5
+ #
6
+ # If you want to register `on_*` callbacks (see ZK::Client::StateMixin)
7
+ # then you should pass a block, which will be called before the
8
+ # connection is set up (this way you can get the `on_connected` event). See
9
+ # the 'Register on_connected callback' example.
10
+ #
11
+ # A note on event delivery. There has been some confusion, caused by
12
+ # incorrect documentation (which I'm very sorry about), about how many
13
+ # threads are delivering events. The documentation for 0.9.0 was incorrect
14
+ # in stating the number of threads used to deliver events. There was one,
15
+ # unconfigurable, event dispatch thread. In 1.0 the number of event
16
+ # delivery threads is configurable, but still defaults to 1.
17
+ #
18
+ # The configurability is intended to allow users to easily dispatch events to
19
+ # event handlers that will perform (application specific) work. Be aware,
20
+ # the default will give you the guarantee that only one event will be delivered
21
+ # at a time. The advantage to this is that you can be sure that no event will
22
+ # be delivered "behind your back" while you're in an event handler. If you're
23
+ # comfortable with dealing with threads and concurrency, then feel free to
24
+ # set the `:threadpool_size` option to the constructor to a value you feel is
25
+ # correct for your app.
26
+ #
27
+ # If you use the threadpool/event callbacks to perform work, you may be
28
+ # interested in registering an `on_exception` callback that will receive
29
+ # all exceptions that occur on the threadpool that are not handled (i.e.
30
+ # that bubble up to top of a block).
31
+ #
32
+ # It is recommended that you not run any possibly long-running work on the
33
+ # event threadpool, as `close!` will attempt to shutdown the threadpool, and
34
+ # **WILL NOT WAIT FOREVER**. (TODO: more on this)
35
+ #
36
+ # @example Register on_connected callback.
37
+ #
38
+ # # the nice thing about this pattern is that in the case of a call to #reopen
39
+ # # all your watches will be re-established
40
+ #
41
+ # ZK::Client::Threaded.new('localhsot:2181') do |zk|
42
+ # # do not do anything in here except register callbacks
43
+ #
44
+ # zk.on_connected do |event|
45
+ # zk.stat('/foo/bar', watch: true)
46
+ # zk.stat('/baz', watch: true)
47
+ # end
48
+ # end
49
+ #
50
+ class Threaded < Base
51
+ include StateMixin
52
+ include Unixisms
53
+ include Conveniences
54
+ include Logging
55
+
56
+ DEFAULT_THREADPOOL_SIZE = 1
57
+
58
+ # Construct a new threaded client.
59
+ #
60
+ # @note The `:timeout` argument here is *not* the session_timeout for the
61
+ # connection. rather it is the amount of time we wait for the connection
62
+ # to be established. The session timeout exchanged with the server is
63
+ # set to 10s by default in the C implemenation, and as of version 0.8.0
64
+ # of slyphon-zookeeper has yet to be exposed as an option. That feature
65
+ # is planned.
66
+ #
67
+ # @note The documentation for 0.9.0 was incorrect in stating the number
68
+ # of threads used to deliver events. There was one, unconfigurable,
69
+ # event dispatch thread. In 1.0 the number of event delivery threads is
70
+ # configurable, but still defaults to 1. (The Management apologizes for
71
+ # any confusion this may have caused).
72
+ #
73
+ # @param [String] host (see ZK::Client::Base#initialize)
74
+ #
75
+ # @option opts [true,false] :reconnect (true) if true, we will register
76
+ # the equivalent of `on_session_expired { zk.reopen }` so that in the
77
+ # case of an expired session, we will keep trying to reestablish the
78
+ # connection.
79
+ #
80
+ # @option opts [Fixnum] :threadpool_size (1) the size of the threadpool that
81
+ # should be used to deliver events. As of 1.0, this is the number of
82
+ # event delivery threads and controls the amount of concurrency in your
83
+ # app if you're doing work in the event callbacks.
84
+ #
85
+ # @option opts [Fixnum] :timeout how long we will wait for the connection
86
+ # to be established. If timeout is nil, we will wait forever *use
87
+ # carefully*.
88
+ #
89
+ # @yield [self] calls the block with the new instance after the event
90
+ # handler and threadpool have been set up, but before any connections
91
+ # have been made. This allows the client to register watchers for
92
+ # session events like `connected`. You *cannot* perform any other
93
+ # operations with the client as you will get a NoMethodError (the
94
+ # underlying connection is nil).
95
+ #
96
+ # @see Base#initialize
97
+ def initialize(host, opts={}, &b)
98
+ super(host, opts)
99
+
100
+ tp_size = opts.fetch(:threadpool_size, DEFAULT_THREADPOOL_SIZE)
101
+ @threadpool = Threadpool.new(tp_size)
102
+
103
+ @session_timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT) # maybe move this into superclass?
104
+ @event_handler = EventHandler.new(self)
105
+
106
+ @reconnect = opts.fetch(:reconnect, true)
107
+
108
+ @mutex = Mutex.new
109
+
110
+ @close_requested = false
111
+
112
+ yield self if block_given?
113
+
114
+ @cnx = create_connection(host, @session_timeout, @event_handler.get_default_watcher_block)
115
+ end
116
+
117
+ # (see Base#reopen)
118
+ def reopen(timeout=nil)
119
+ @mutex.synchronize { @close_requested = false }
120
+ super
121
+ end
122
+
123
+ # (see Base#close!)
124
+ #
125
+ # @note We will make our best effort to do the right thing if you call
126
+ # this method while in the threadpool. It is _a much better idea_ to
127
+ # call us from the main thread, or _at least_ a thread we're not going
128
+ # to be trying to shut down as part of closing the connection and
129
+ # threadpool.
130
+ #
131
+ def close!
132
+ @mutex.synchronize do
133
+ return if @close_requested
134
+ @close_requested = true
135
+ end
136
+
137
+ on_tpool = on_threadpool?
138
+
139
+ # Ok, so the threadpool will wait up to N seconds while joining each thread.
140
+ # If _we're on a threadpool thread_, have it wait until we're ready to jump
141
+ # out of this method, and tell it to wait up to 5 seconds to let us get
142
+ # clear, then do the rest of the shutdown of the connection
143
+ #
144
+ # if the user *doesn't* hate us, then we just join the shutdown_thread immediately
145
+ # and wait for it to exit
146
+ #
147
+ shutdown_thread = Thread.new do
148
+ @threadpool.shutdown(2)
149
+ super
150
+ end
151
+
152
+ shutdown_thread.join unless on_tpool
153
+
154
+ nil
155
+ end
156
+
157
+ # (see Threadpool#on_threadpool?)
158
+ def on_threadpool?
159
+ @threadpool and @threadpool.on_threadpool?
160
+ end
161
+
162
+ # (see Threadpool#on_exception)
163
+ def on_exception(&blk)
164
+ @threadpool.on_exception(&blk)
165
+ end
166
+
167
+ # @private
168
+ def raw_event_handler(event)
169
+ return unless event.session_event?
170
+
171
+ if event.client_invalid?
172
+ return unless @reconnect
173
+
174
+ @mutex.synchronize do
175
+ unless @close_requested # a legitimate shutdown case
176
+
177
+ logger.error { "Got event #{event.state_name}, calling reopen(0)! things may be messed up until this works itself out!" }
178
+
179
+ # reopen(0) means that we don't want to wait for the connection
180
+ # to reach the connected state before returning
181
+ reopen(0)
182
+ end
183
+ end
184
+ end
185
+ rescue Exception => e
186
+ logger.error { "BUG: Exception caught in raw_event_handler: #{e.to_std_format}" }
187
+ end
188
+
189
+ protected
190
+ # @private
191
+ def create_connection(*args)
192
+ ::Zookeeper.new(*args)
193
+ end
194
+ end
195
+ end
196
+ end
File without changes
data/lib/zk/client.rb ADDED
@@ -0,0 +1,59 @@
1
+ module ZK
2
+ # A ruby-friendly wrapper around the low-level zookeeper drivers.
3
+ #
4
+ # You're probably looking for {Client::Base} and {Client::Threaded}.
5
+ #
6
+ # Once you've had a look there, take a look at {Client::Conveniences},
7
+ # {Client::StateMixin}, and {Client::Unixisms}
8
+ #
9
+ # @todo ACL support is pretty much unused currently.
10
+ # If anyone has suggestions, hints, use-cases, examples, etc. by all means please file a bug.
11
+ #
12
+ module Client
13
+ DEFAULT_TIMEOUT = 10 unless defined?(DEFAULT_TIMEOUT)
14
+
15
+ # @private
16
+ STATE_SYM_MAP = {
17
+ Zookeeper::ZOO_CLOSED_STATE => :closed,
18
+ Zookeeper::ZOO_EXPIRED_SESSION_STATE => :expired_session,
19
+ Zookeeper::ZOO_AUTH_FAILED_STATE => :auth_failed,
20
+ Zookeeper::ZOO_CONNECTING_STATE => :connecting,
21
+ Zookeeper::ZOO_CONNECTED_STATE => :connected,
22
+ Zookeeper::ZOO_ASSOCIATING_STATE => :associating,
23
+ }.freeze unless defined?(STATE_SYM_MAP)
24
+
25
+ class << self
26
+ def new(*a, &b)
27
+ Threaded.new(*a, &b)
28
+ end
29
+
30
+ # @private
31
+ def assert_valid_chroot_str!(str)
32
+ return unless str
33
+ raise ChrootMustStartWithASlashError, str unless str.start_with?('/')
34
+ end
35
+
36
+ # Takes a connection string and returns an Array of [host, chroot_path].
37
+ # If the connection string is not chrooted, then chroot_path will be nil.
38
+ #
39
+ # @private
40
+ def split_chroot(str)
41
+ if idx = str.index('/')
42
+ host = str[0...idx]
43
+ chroot_path = str[idx..-1]
44
+
45
+ [host, chroot_path]
46
+ else
47
+ [str, nil]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ require 'zk/client/state_mixin'
55
+ require 'zk/client/unixisms'
56
+ require 'zk/client/conveniences'
57
+ require 'zk/client/base'
58
+ require 'zk/client/threaded'
59
+
@@ -0,0 +1,75 @@
1
+ # @private
2
+ class ::Exception
3
+ unless method_defined?(:to_std_format)
4
+ def to_std_format
5
+ ary = ["#{self.class}: #{message}"]
6
+ ary.concat(backtrace || [])
7
+ ary.join("\n\t")
8
+ end
9
+ end
10
+ end
11
+
12
+ # @private
13
+ class ::Thread
14
+ def zk_mongoid_lock_registry
15
+ self[:_zk_mongoid_lock_registry]
16
+ end
17
+
18
+ def zk_mongoid_lock_registry=(obj)
19
+ self[:_zk_mongoid_lock_registry] = obj
20
+ end
21
+ end
22
+
23
+ # @private
24
+ class ::Hash
25
+ # taken from ActiveSupport 3.0.12, but we don't replace it if it exists
26
+ unless method_defined?(:extractable_options?)
27
+ def extractable_options?
28
+ instance_of?(Hash)
29
+ end
30
+ end
31
+ end
32
+
33
+ # @private
34
+ class ::Array
35
+ unless method_defined?(:extract_options!)
36
+ def extract_options!
37
+ if last.is_a?(Hash) && last.extractable_options?
38
+ pop
39
+ else
40
+ {}
41
+ end
42
+ end
43
+ end
44
+
45
+ # backport this from 1.9.x to 1.8.7
46
+ #
47
+ # this obviously cannot replicate the copy-on-write semantics of the
48
+ # 1.9.3 version, and only provides a naieve filtering functionality.
49
+ #
50
+ # also, does not handle the "returning an enumerator" case
51
+ unless method_defined?(:select!)
52
+ def select!(&b)
53
+ replace(select(&b))
54
+ end
55
+ end
56
+ end
57
+
58
+ # @private
59
+ module ::Kernel
60
+ unless method_defined?(:silence_warnings)
61
+ def silence_warnings
62
+ with_warnings(nil) { yield }
63
+ end
64
+ end
65
+
66
+ unless method_defined?(:with_warnings)
67
+ def with_warnings(flag)
68
+ old_verbose, $VERBOSE = $VERBOSE, flag
69
+ yield
70
+ ensure
71
+ $VERBOSE = old_verbose
72
+ end
73
+ end
74
+ end
75
+
File without changes
data/lib/zk/event.rb ADDED
@@ -0,0 +1,168 @@
1
+ module ZK
2
+ # Provides most of the functionality ZK uses around events. Base class is actually
3
+ # [ZookeeperCallbacks::WatcherCallback](http://rubydoc.info/gems/slyphon-zookeeper/ZookeeperCallbacks/WatcherCallback),
4
+ # but this module is mixed in and provides a lot of useful syntactic sugar.
5
+ #
6
+ module Event
7
+ include ZookeeperConstants
8
+
9
+ # unless defined? apparently messes up yard's ability to see the @private
10
+ silence_warnings do
11
+ # XXX: this is not uesd it seems
12
+ # @private
13
+ EVENT_NAME_MAP = {
14
+ 1 => 'created',
15
+ 2 => 'deleted',
16
+ 3 => 'changed',
17
+ 4 => 'child',
18
+ -1 => 'session',
19
+ -2 => 'notwatching',
20
+ }.freeze
21
+
22
+ # @private
23
+ STATES = %w[connecting associating connected auth_failed expired_session].freeze
24
+
25
+ # @private
26
+ EVENT_TYPES = %w[created deleted changed child session notwatching].freeze
27
+ end
28
+
29
+ # The numeric constant (one of `ZOO_*_EVENT`) that ZooKeeper sets to
30
+ # indicate the type of event this is. Users are advised to use the '?'
31
+ # methods below instead of using this value.
32
+ #
33
+ # @return [Fixnum]
34
+ def type
35
+ # no-op, the functionality is provided by the class this is mixed into.
36
+ # here only for documentation purposes
37
+ end
38
+
39
+ # The numeric constant (one of `ZOO_*_STATE`) that ZooKeeper sets to
40
+ # indicate the session state this event is notifying us of. Users are
41
+ # encouraged to use the '?' methods below, instead of this value.
42
+ #
43
+ # @return [Fixnum]
44
+ def state
45
+ # no-op, the functionality is provided by the class this is mixed into.
46
+ # here only for documentation purposes
47
+ end
48
+
49
+ # The path this event is in reference to.
50
+ #
51
+ # @return [String,nil] This value will be nil if `session_event?` is false, otherwise
52
+ # a String containing the path this event was triggered in reference to
53
+ def path
54
+ # no-op, the functionality is provided by the class this is mixed into.
55
+ # here only for documentation purposes
56
+ end
57
+
58
+ # Is this event notifying us we're in the connecting state?
59
+ def connecting?
60
+ @state == ZOO_CONNECTING_STATE
61
+ end
62
+ alias state_connecting? connecting?
63
+
64
+ # Is this event notifying us we're in the associating state?
65
+ def associating?
66
+ @state == ZOO_ASSOCIATING_STATE
67
+ end
68
+ alias state_associating? associating?
69
+
70
+ # Is this event notifying us we're in the connected state?
71
+ def connected?
72
+ @state == ZOO_CONNECTED_STATE
73
+ end
74
+ alias state_connected? connected?
75
+
76
+ # Is this event notifying us we're in the auth_failed state?
77
+ def auth_failed?
78
+ @state == ZOO_AUTH_FAILED_STATE
79
+ end
80
+ alias state_auth_failed? auth_failed?
81
+
82
+ # Is this event notifying us we're in the expired_session state?
83
+ def expired_session?
84
+ @state == ZOO_EXPIRED_SESSION_STATE
85
+ end
86
+ alias state_expired_session? expired_session?
87
+
88
+ # return this event's state name as a string "ZOO_*_STATE", used for debugging
89
+ def state_name
90
+ (name = STATE_NAMES[@state]) ? "ZOO_#{name.to_s.upcase}_STATE" : ''
91
+ end
92
+
93
+ # Has a node been created?
94
+ def node_created?
95
+ @type == ZOO_CREATED_EVENT
96
+ end
97
+
98
+ # Has a node been deleted?
99
+ def node_deleted?
100
+ @type == ZOO_DELETED_EVENT
101
+ end
102
+
103
+ # Has a node changed?
104
+ def node_changed?
105
+ @type == ZOO_CHANGED_EVENT
106
+ end
107
+
108
+ # Has a node's list of children changed?
109
+ def node_child?
110
+ @type == ZOO_CHILD_EVENT
111
+ end
112
+
113
+ # Is this a session-related event?
114
+ #
115
+ # @deprecated This was an artifact of the way these methods were created
116
+ # originally, will be removed because it's kinda dumb. use {#session_event?}
117
+ def node_session?
118
+ @type == ZOO_SESSION_EVENT
119
+ end
120
+
121
+ # I have never seen this event delivered. here for completeness.
122
+ def node_notwatching?
123
+ @type == ZOO_NOTWATCHING_EVENT
124
+ end
125
+ alias node_not_watching? node_notwatching?
126
+
127
+ # return this event's type name as a string "ZOO_*_EVENT", used for debugging
128
+ def event_name
129
+ (name = EVENT_TYPE_NAMES[@type]) ? "ZOO_#{name.to_s.upcase}_EVENT" : ''
130
+ end
131
+
132
+ # used by the EventHandler
133
+ # @private
134
+ def interest_key
135
+ EVENT_TYPE_NAMES.fetch(@type).to_sym
136
+ end
137
+
138
+ # has this watcher been called because of a change in connection state?
139
+ def session_event?
140
+ @type == ZOO_SESSION_EVENT
141
+ end
142
+ alias state_event? session_event?
143
+
144
+ # has this watcher been called because of a change to a zookeeper node?
145
+ # `node_event?` and `session_event?` are mutually exclusive.
146
+ def node_event?
147
+ path and not path.empty?
148
+ end
149
+
150
+ # according to [the programmer's guide](http://zookeeper.apache.org/doc/r3.3.4/zookeeperProgrammers.html#Java+Binding)
151
+ #
152
+ # > once a ZooKeeper object is closed or receives a fatal event
153
+ # > (SESSION_EXPIRED and AUTH_FAILED), the ZooKeeper object becomes
154
+ # > invalid.
155
+ #
156
+ # this will return true for either of those cases
157
+ #
158
+ def client_invalid?
159
+ (@state == ZOO_EXPIRED_SESSION_STATE) || (@state == ZOO_AUTH_FAILED_STATE)
160
+ end
161
+ end
162
+ end
163
+
164
+ # @private
165
+ class ZookeeperCallbacks::WatcherCallback
166
+ include ::ZK::Event
167
+ end
168
+
@@ -10,6 +10,8 @@ module ZK
10
10
 
11
11
  VALID_WATCH_TYPES = [:data, :child].freeze
12
12
 
13
+ ALL_NODE_EVENTS_KEY = :all_node_events
14
+
13
15
  ZOOKEEPER_WATCH_TYPE_MAP = {
14
16
  Zookeeper::ZOO_CREATED_EVENT => :data,
15
17
  Zookeeper::ZOO_DELETED_EVENT => :data,
@@ -17,7 +19,8 @@ module ZK
17
19
  Zookeeper::ZOO_CHILD_EVENT => :child,
18
20
  }.freeze
19
21
 
20
- attr_accessor :zk # :nodoc:
22
+ # @private
23
+ attr_accessor :zk
21
24
 
22
25
  # @private
23
26
  # :nodoc:
@@ -33,9 +36,10 @@ module ZK
33
36
  end
34
37
 
35
38
  # @see ZK::Client::Base#register
36
- def register(path, &block)
37
- # logger.debug { "EventHandler#register path=#{path.inspect}" }
38
- EventHandlerSubscription.new(self, path, block).tap do |subscription|
39
+ def register(path, interests=nil, &block)
40
+ path = ALL_NODE_EVENTS_KEY if path == :all
41
+
42
+ EventHandlerSubscription.new(self, path, block, interests).tap do |subscription|
39
43
  synchronize { @callbacks[path] << subscription }
40
44
  end
41
45
  end
@@ -98,14 +102,16 @@ module ZK
98
102
  #
99
103
  # @private
100
104
  def process(event)
105
+ @zk.raw_event_handler(event)
106
+
101
107
  # logger.debug { "EventHandler#process dispatching event: #{event.inspect}" }# unless event.type == -1
102
108
  event.zk = @zk
103
109
 
104
- cb_key =
110
+ cb_keys =
105
111
  if event.node_event?
106
- event.path
112
+ [event.path, ALL_NODE_EVENTS_KEY]
107
113
  elsif event.state_event?
108
- state_key(event.state)
114
+ [state_key(event.state)]
109
115
  else
110
116
  raise ZKError, "don't know how to process event: #{event.inspect}"
111
117
  end
@@ -113,23 +119,40 @@ module ZK
113
119
  # logger.debug { "EventHandler#process: cb_key: #{cb_key}" }
114
120
 
115
121
  cb_ary = synchronize do
116
- if event.node_event?
117
- if watch_type = ZOOKEEPER_WATCH_TYPE_MAP[event.type]
118
- # logger.debug { "re-allowing #{watch_type.inspect} watches on path #{event.path.inspect}" }
119
-
120
- # we recieved a watch event for this path, now we allow code to set new watchers
121
- @outstanding_watches[watch_type].delete(event.path)
122
- end
123
- end
122
+ clear_watch_restrictions(event)
124
123
 
125
- @callbacks[cb_key].dup
124
+ @callbacks.values_at(*cb_keys)
126
125
  end
127
126
 
127
+ cb_ary.flatten! # takes care of not modifying original arrays
128
128
  cb_ary.compact!
129
129
 
130
+ # we only filter for node events
131
+ if event.node_event?
132
+ interest_key = event.interest_key
133
+ cb_ary.select! { |sub| sub.interests.include?(interest_key) }
134
+ end
135
+
130
136
  safe_call(cb_ary, event)
131
137
  end
132
138
 
139
+ private
140
+ # happens inside the lock, clears the restriction on setting new watches
141
+ # for a given path/event type combination
142
+ #
143
+ def clear_watch_restrictions(event)
144
+ return unless event.node_event?
145
+
146
+ if watch_type = ZOOKEEPER_WATCH_TYPE_MAP[event.type]
147
+ #logger.debug { "re-allowing #{watch_type.inspect} watches on path #{event.path.inspect}" }
148
+
149
+ # we recieved a watch event for this path, now we allow code to set new watchers
150
+ @outstanding_watches[watch_type].delete(event.path)
151
+ end
152
+ end
153
+
154
+ public
155
+
133
156
  # used during shutdown to clear registered listeners
134
157
  # @private
135
158
  def clear! #:nodoc:
@@ -137,7 +160,7 @@ module ZK
137
160
  @callbacks.clear
138
161
  nil
139
162
  end
140
- end
163
+ end
141
164
 
142
165
  # @private
143
166
  def synchronize
@@ -171,8 +194,9 @@ module ZK
171
194
  # fired. This prevents one event delivery to *every* callback per :watch => true
172
195
  # argument.
173
196
  #
174
- # due to somewhat poor design, we destructively modify opts before we yield
175
- # and the client implictly knows this
197
+ # due to arguably poor design, we destructively modify opts before we yield
198
+ # and the client implictly knows this (this method constitutes some of the option
199
+ # parsing for the base class methods)
176
200
  #
177
201
  # @private
178
202
  def setup_watcher!(watch_type, opts)
@@ -228,15 +252,16 @@ module ZK
228
252
 
229
253
  # @private
230
254
  def safe_call(callbacks, *args)
231
- while cb = callbacks.shift
232
- begin
233
- cb.call(*args) if cb.respond_to?(:call)
234
- rescue Exception => e
235
- logger.error { "Error caught in user supplied callback" }
236
- logger.error { e.to_std_format }
237
- end
255
+ # oddly, a `while cb = callbacks.shift` here will have thread safety issues
256
+ # as cb will be nil when the defer block is called on the threadpool
257
+
258
+ callbacks.each do |cb|
259
+ next unless cb.respond_to?(:call)
260
+
261
+ zk.defer { cb.call(*args) }
238
262
  end
239
263
  end
240
- end
241
- end
264
+
265
+ end # EventHandler
266
+ end # ZK
242
267