zk 1.4.2 → 1.5.0

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