zk 1.4.2 → 1.5.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.
Files changed (49) hide show
  1. data/.dotfiles/ctags_paths +1 -0
  2. data/.dotfiles/rspec-logging +2 -2
  3. data/.gitignore +1 -0
  4. data/Gemfile +9 -3
  5. data/Guardfile +36 -0
  6. data/README.markdown +21 -18
  7. data/RELEASES.markdown +10 -0
  8. data/Rakefile +1 -1
  9. data/lib/zk.rb +28 -21
  10. data/lib/zk/client/threaded.rb +107 -17
  11. data/lib/zk/client/unixisms.rb +1 -41
  12. data/lib/zk/core_ext.rb +28 -0
  13. data/lib/zk/election.rb +14 -3
  14. data/lib/zk/event_handler.rb +36 -37
  15. data/lib/zk/event_handler_subscription/actor.rb +37 -2
  16. data/lib/zk/event_handler_subscription/base.rb +9 -0
  17. data/lib/zk/exceptions.rb +5 -0
  18. data/lib/zk/fork_hook.rb +112 -0
  19. data/lib/zk/install_fork_hooks.rb +37 -0
  20. data/lib/zk/locker/exclusive_locker.rb +14 -10
  21. data/lib/zk/locker/locker_base.rb +43 -26
  22. data/lib/zk/locker/shared_locker.rb +9 -5
  23. data/lib/zk/logging.rb +29 -7
  24. data/lib/zk/node_deletion_watcher.rb +167 -0
  25. data/lib/zk/pool.rb +14 -4
  26. data/lib/zk/subscription.rb +15 -34
  27. data/lib/zk/threaded_callback.rb +113 -29
  28. data/lib/zk/threadpool.rb +136 -40
  29. data/lib/zk/version.rb +1 -1
  30. data/spec/logging_progress_bar_formatter.rb +12 -0
  31. data/spec/shared/client_contexts.rb +13 -1
  32. data/spec/shared/client_examples.rb +3 -1
  33. data/spec/spec_helper.rb +28 -3
  34. data/spec/support/client_forker.rb +49 -8
  35. data/spec/support/latch.rb +1 -19
  36. data/spec/support/logging.rb +26 -10
  37. data/spec/support/wait_watchers.rb +2 -2
  38. data/spec/zk/00_forked_client_integration_spec.rb +1 -1
  39. data/spec/zk/client_spec.rb +11 -2
  40. data/spec/zk/election_spec.rb +21 -7
  41. data/spec/zk/locker_spec.rb +42 -22
  42. data/spec/zk/node_deletion_watcher_spec.rb +69 -0
  43. data/spec/zk/pool_spec.rb +32 -18
  44. data/spec/zk/threaded_callback_spec.rb +78 -0
  45. data/spec/zk/threadpool_spec.rb +52 -0
  46. data/spec/zk/watch_spec.rb +4 -0
  47. data/zk.gemspec +2 -1
  48. metadata +36 -10
  49. data/spec/support/logging_progress_bar_formatter.rb +0 -14
@@ -127,51 +127,11 @@ module ZK
127
127
  # a deleted event. Includes the {InterruptedSession} module.
128
128
  #
129
129
  def block_until_node_deleted(abs_node_path)
130
- subs = []
131
-
132
130
  assert_we_are_not_on_the_event_dispatch_thread!
133
131
 
134
132
  raise ArgumentError, "argument must be String-ish, not: #{abs_node_path.inspect}" unless abs_node_path
135
133
 
136
- queue = Queue.new
137
-
138
- node_deletion_cb = lambda do |event|
139
- if event.node_deleted?
140
- queue.enq(:deleted)
141
- else
142
- queue.enq(:deleted) unless exists?(abs_node_path, :watch => true)
143
- end
144
- end
145
-
146
- subs << event_handler.register(abs_node_path, &node_deletion_cb)
147
-
148
- # NOTE: this pattern may be necessary for other features with blocking semantics!
149
-
150
- session_cb = lambda do |event|
151
- queue.enq(event.state)
152
- end
153
-
154
- [:expired_session, :connecting, :closed].each do |sym|
155
- subs << event_handler.register_state_handler(sym, &session_cb)
156
- end
157
-
158
- # set up the callback, but bail if we don't need to wait
159
- return true unless exists?(abs_node_path, :watch => true)
160
-
161
- case queue.pop
162
- when :deleted
163
- true
164
- when ZOO_EXPIRED_SESSION_STATE
165
- raise Zookeeper::Exceptions::SessionExpired
166
- when ZOO_CONNECTING_STATE
167
- raise Zookeeper::Exceptions::NotConnected
168
- when ZOO_CLOSED_STATE
169
- raise Zookeeper::Exceptions::ConnectionClosed
170
- else
171
- raise "Hit unexpected case in block_until_node_deleted"
172
- end
173
- ensure
174
- subs.each(&:unregister)
134
+ NodeDeletionWatcher.new(self, abs_node_path).block_until_deleted
175
135
  end
176
136
  end
177
137
  end
@@ -99,4 +99,32 @@ class ::Module
99
99
  end
100
100
  end
101
101
 
102
+ require 'logger'
103
+
104
+ # lets you clone a a Logger instance but change properties, this is
105
+ # used by the test suite to change the progname for different components
106
+ # @private
107
+ class ::Logger
108
+ unless method_defined?(:clone_new_log)
109
+ attr_writer :logdev
110
+
111
+ def clone_new_log(opts={})
112
+ self.class.new(nil).tap do |noo_log|
113
+ noo_log.progname = opts.fetch(:progname, self.progname)
114
+ noo_log.formatter = opts.fetch(:formatter, self.formatter)
115
+ noo_log.level = opts.fetch(:level, self.level)
116
+ noo_log.logdev = @logdev
117
+ end
118
+ end
119
+ end
120
+
121
+ def debug_pp(title)
122
+ debug do
123
+ str = "---< #{title} >---\n"
124
+ require 'pp'
125
+ str << PP.pp(yield, '')
126
+ end
127
+ end
128
+ end
129
+
102
130
 
@@ -1,7 +1,5 @@
1
1
  module ZK
2
- # NOTE: this module should be considered experimental. there are several
3
- # specs that have recently started failing under 1.9.2 (didn't fail under
4
- # 1.8.7 or jruby 1.6) that need fixing.
2
+ # NOTE: this module should be considered experimental.
5
3
  #
6
4
  # ==== Overview
7
5
  #
@@ -72,6 +70,14 @@ module ZK
72
70
  opts = DEFAULT_OPTS.merge(opts)
73
71
  @root_election_node = opts[:root_election_node]
74
72
  @mutex = Monitor.new
73
+ @closed = false
74
+ end
75
+
76
+ def close
77
+ @mutex.synchronize do
78
+ return if @closed
79
+ @closed = true
80
+ end
75
81
  end
76
82
 
77
83
  # holds the ephemeral nodes of this election
@@ -109,6 +115,7 @@ module ZK
109
115
  # role.
110
116
  def on_leader_ack(&block)
111
117
  creation_sub = @zk.register(leader_ack_path, :only => [:created, :changed]) do |event|
118
+ return if @closed
112
119
  begin
113
120
  logger.debug { "in #{leader_ack_path} watcher, got creation event, notifying" }
114
121
  safe_call(block)
@@ -119,6 +126,7 @@ module ZK
119
126
 
120
127
  deletion_sub = @zk.register(leader_ack_path, :only => [:deleted, :child]) do |event|
121
128
  if @zk.exists?(leader_ack_path, :watch => true)
129
+ return if @closed
122
130
  begin
123
131
  logger.debug { "in #{leader_ack_path} watcher, node created behind our back, notifying" }
124
132
  safe_call(block)
@@ -269,12 +277,15 @@ module ZK
269
277
  end
270
278
 
271
279
  def handle_winning_election
280
+ @mutex.synchronize { return if @closed }
272
281
  @leader = true
273
282
  fire_winning_callbacks!
274
283
  acknowledge_win!
275
284
  end
276
285
 
277
286
  def handle_losing_election(our_idx, ballots)
287
+ @mutex.synchronize { return if @closed }
288
+
278
289
  @leader = false
279
290
 
280
291
  on_leader_ack do
@@ -33,6 +33,8 @@ module ZK
33
33
  def initialize(zookeeper_client, opts={})
34
34
  @zk = zookeeper_client
35
35
 
36
+ @orig_pid = Process.pid
37
+
36
38
  @thread_opt = opts.fetch(:thread, :single)
37
39
  EventHandlerSubscription.class_for_thread_option(@thread_opt) # this is side-effecty, will raise an ArgumentError if given a bad value.
38
40
 
@@ -48,42 +50,17 @@ module ZK
48
50
 
49
51
  reopen_after_fork!
50
52
  end
51
-
52
- # stops the dispatching of events. already in-flight callbacks may still be running, but
53
- # no new events will be dispatched until {#start} is called
54
- #
55
- # any events that are delivered while we are stopped will be lost
56
- #
57
- def stop
58
- synchronize do
59
- return if @state == :stopped
60
- @state = :stopped
61
- end
62
- end
63
-
64
- # called to re-enable event delivery
65
- def start
66
- synchronize do
67
- return if @state == :running
68
- @state = :running
69
- end
70
- end
71
-
72
- def running?
73
- synchronize { @state == :running }
74
- end
75
-
76
- def stopped?
77
- synchronize { @state == :stopped }
78
- end
79
-
53
+
80
54
  # do not call this method. it is inteded for use only when we've forked and
81
55
  # all other threads are dead.
82
56
  #
83
57
  # @private
84
58
  def reopen_after_fork!
59
+ # logger.debug { "#{self.class}##{__method__}" }
85
60
  @mutex = Monitor.new
86
61
  # XXX: need to test this w/ actor-style callbacks
62
+
63
+ @state = :running
87
64
  @callbacks.values.flatten.each { |cb| cb.reopen_after_fork! if cb.respond_to?(:reopen_after_fork!) }
88
65
  @outstanding_watches.values.each { |set| set.clear }
89
66
  nil
@@ -164,7 +141,7 @@ module ZK
164
141
  end
165
142
  alias :unsubscribe :unregister
166
143
 
167
- # called from the client-registered callback when an event fires
144
+ # called from the Client registered callback when an event fires
168
145
  #
169
146
  # @note this is *ONLY* dealing with asynchronous callbacks! watchers
170
147
  # and session events go through here, NOT anything else!!
@@ -173,7 +150,7 @@ module ZK
173
150
  def process(event)
174
151
  @zk.raw_event_handler(event)
175
152
 
176
- # logger.debug { "EventHandler#process dispatching event: #{event.inspect}" }# unless event.type == -1
153
+ logger.debug { "EventHandler#process dispatching event: #{event.inspect}" }# unless event.type == -1
177
154
  event.zk = @zk
178
155
 
179
156
  cb_keys =
@@ -185,8 +162,6 @@ module ZK
185
162
  raise ZKError, "don't know how to process event: #{event.inspect}"
186
163
  end
187
164
 
188
- # logger.debug { "EventHandler#process: cb_key: #{cb_key}" }
189
-
190
165
  cb_ary = synchronize do
191
166
  clear_watch_restrictions(event)
192
167
 
@@ -235,10 +210,34 @@ module ZK
235
210
  def close
236
211
  synchronize do
237
212
  @callbacks.values.flatten.each(&:close)
213
+ @state = :closed
238
214
  clear!
239
215
  end
240
216
  end
241
217
 
218
+ # @private
219
+ def pause_before_fork_in_parent
220
+ synchronize do
221
+ raise InvalidStateError, "invalid state, expected to be :running, was #{@state.inspect}" if @state != :running
222
+ return false if @state == :paused
223
+ @state = :paused
224
+ end
225
+ logger.debug { "#{self.class}##{__method__}" }
226
+
227
+ @callbacks.values.flatten.each(&:pause_before_fork_in_parent)
228
+ end
229
+
230
+ # @private
231
+ def resume_after_fork_in_parent
232
+ synchronize do
233
+ raise InvalidStateError, "expected :paused, was #{@state.inspect}" if @state != :paused
234
+ @state = :running
235
+ end
236
+ logger.debug { "#{self.class}##{__method__}" }
237
+
238
+ @callbacks.values.flatten.each(&:resume_after_fork_in_parent)
239
+ end
240
+
242
241
  # @private
243
242
  def synchronize
244
243
  @mutex.synchronize { yield }
@@ -329,16 +328,16 @@ module ZK
329
328
 
330
329
  # @private
331
330
  def safe_call(callbacks, *args)
332
- # oddly, a `while cb = callbacks.shift` here will have thread safety issues
333
- # as cb will be nil when the defer block is called on the threadpool
334
-
335
331
  callbacks.each do |cb|
336
332
  next unless cb.respond_to?(:call)
337
333
 
338
334
  if cb.async?
339
335
  cb.call(*args)
340
336
  else
341
- zk.defer { cb.call(*args) }
337
+ zk.defer do
338
+ logger.debug { "called #{cb.inspect} with #{args.inspect} on threadpool" }
339
+ cb.call(*args)
340
+ end
342
341
  end
343
342
  end
344
343
  end
@@ -16,15 +16,50 @@ module ZK
16
16
  # guarantees), just perhaps at different times.
17
17
  #
18
18
  class Actor < Base
19
- include Subscription::ActorStyle
19
+ # @private
20
+ attr_reader :threaded_callback
21
+
22
+ def initialize(parent, path, callback, opts={})
23
+ super
24
+ @threaded_callback = ThreadedCallback.new(@callable)
25
+ end
20
26
 
21
27
  def async?
22
28
  true
23
29
  end
24
30
 
31
+ def call(*args)
32
+ @threaded_callback.call(*args)
33
+ end
34
+
25
35
  # calls unsubscribe and shuts down
26
36
  def close
27
- unsubscribe
37
+ unregister
38
+ end
39
+
40
+ def unregister
41
+ super
42
+ @threaded_callback.shutdown
43
+ end
44
+
45
+ def reopen_after_fork!
46
+ logger.debug { "#{self.class}##{__method__}" }
47
+ super
48
+ @threaded_callback.reopen_after_fork!
49
+ end
50
+
51
+ def pause_before_fork_in_parent
52
+ synchronize do
53
+ logger.debug { "#{self.class}##{__method__}" }
54
+ @threaded_callback.pause_before_fork_in_parent
55
+ super
56
+ end
57
+ end
58
+
59
+ def resume_after_fork_in_parent
60
+ super
61
+ logger.debug { "#{self.class}##{__method__}" }
62
+ @threaded_callback.resume_after_fork_in_parent
28
63
  end
29
64
  end
30
65
  end
@@ -43,6 +43,15 @@ module ZK
43
43
  def close
44
44
  end
45
45
 
46
+ # stop anything non-fork-safe in parent
47
+ def pause_before_fork_in_parent
48
+ end
49
+
50
+ # take any action necessary to deliver events after a fork
51
+ # in the parent
52
+ def resume_after_fork_in_parent
53
+ end
54
+
46
55
  protected
47
56
  def prep_interests(a)
48
57
  # logger.debug { "prep_interests: #{a.inspect}" }
@@ -129,6 +129,11 @@ module ZK
129
129
  # raised when someone calls lock.assert! but they do not hold the lock
130
130
  class LockAssertionFailedError < ZKError; end
131
131
 
132
+ # called when the client is reopened, resumed, or paused when in an invalid state
133
+ class InvalidStateError < ZKError; end
134
+
135
+ class WakeUpException < ZKError; end
136
+
132
137
  # raised when a chrooted conection is requested but the root doesn't exist
133
138
  class ChrootPathDoesNotExistError < NoNode
134
139
  def initialize(host_string, chroot_path)
@@ -0,0 +1,112 @@
1
+ module ZK
2
+ module ForkHook
3
+ include ZK::Logging
4
+ extend self
5
+
6
+ @mutex = Mutex.new unless @mutex
7
+
8
+ @hooks = {
9
+ :prepare => [],
10
+ :after_child => [],
11
+ :after_parent => [],
12
+ } unless @hooks
13
+
14
+ attr_reader :hooks, :mutex
15
+
16
+ # @private
17
+ def fire_prepare_hooks!
18
+ @mutex.lock
19
+ safe_call(@hooks[:prepare])
20
+ end
21
+
22
+ # @private
23
+ def fire_after_child_hooks!
24
+ safe_call(@hooks[:after_child])
25
+ ensure
26
+ @mutex.unlock rescue nil
27
+ end
28
+
29
+ # @private
30
+ def fire_after_parent_hooks!
31
+ safe_call(@hooks[:after_parent])
32
+ ensure
33
+ @mutex.unlock rescue nil
34
+ end
35
+
36
+ # @private
37
+ def clear!
38
+ @mutex.synchronize { @hooks.values(&:clear) }
39
+ end
40
+
41
+ # @private
42
+ def unregister(sub)
43
+ @mutex.synchronize do
44
+ @hooks.fetch(sub.hook_type, []).delete(sub)
45
+ end
46
+ end
47
+
48
+ # do :call on each of callbacks. if a WeakRef::RefError
49
+ # is caught, modify `callbacks` by removing the dud reference
50
+ #
51
+ # @private
52
+ def safe_call(callbacks)
53
+ cbs = callbacks.dup
54
+
55
+ while cb = cbs.shift
56
+ begin
57
+ cb.call
58
+ rescue WeakRef::RefError
59
+ # clean weakrefs out of the original callback arrays if they're bad
60
+ callbacks.delete(cb)
61
+ rescue Exception => e
62
+ logger.error { e.to_std_format }
63
+ end
64
+ end
65
+ end
66
+
67
+ # @private
68
+ def register(hook_type, block)
69
+ unless hooks.has_key?(hook_type)
70
+ raise "Invalid hook type specified: #{hook.inspect}"
71
+ end
72
+
73
+ unless block.respond_to?(:call)
74
+ raise ArgumentError, "You must provide either a callable an argument or a block"
75
+ end
76
+
77
+ ForkSubscription.new(hook_type, block).tap do |sub|
78
+ # use a WeakRef so that the original objects can be GC'd
79
+ @mutex.synchronize { @hooks[hook_type] << WeakRef.new(sub) }
80
+ end
81
+ end
82
+
83
+ # Register a block that will be called in the parent process before a fork() occurs
84
+ def prepare_for_fork(callable=nil, &blk)
85
+ register(:prepare, callable || blk)
86
+ end
87
+
88
+ # register a block that will be called after the fork happens in the parent process
89
+ def after_fork_in_parent(callable=nil, &blk)
90
+ register(:after_parent, callable || blk)
91
+ end
92
+
93
+ # register a block that will be called after the fork happens in the child process
94
+ def after_fork_in_child(callable=nil, &blk)
95
+ register(:after_child, callable || blk)
96
+ end
97
+
98
+ class ForkSubscription < Subscription::Base
99
+ attr_reader :hook_type
100
+
101
+ def initialize(hook_type, block)
102
+ super(ForkHook, block)
103
+
104
+ @hook_type = hook_type
105
+ end
106
+ end # ForkSubscription
107
+ end # ForkHook
108
+
109
+ def self.install_fork_hook
110
+ require 'zk/install_fork_hooks'
111
+ end
112
+ end # ZK