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
@@ -0,0 +1 @@
1
+ ~/vendor/ruby/*.{c,h}
@@ -1,4 +1,4 @@
1
1
  --color
2
- --require ./spec/support/logging_progress_bar_formatter.rb
3
- --format Motionbox::LoggingProgressBarFormatter
2
+ --require ./spec/logging_progress_bar_formatter.rb
3
+ --format LoggingProgressBarFormatter
4
4
 
data/.gitignore CHANGED
@@ -10,3 +10,4 @@ Gemfile.*
10
10
  wiki
11
11
  zookeeper
12
12
  coverage/
13
+ .ctags_paths
data/Gemfile CHANGED
@@ -8,11 +8,10 @@ source :rubygems
8
8
  # keep closer track of this stuff to make bisecting easier and travis more
9
9
  # accurate
10
10
 
11
- # git 'git://github.com/slyphon/zookeeper.git', :tag => 'dev/zk/00001' do
12
- # gem 'zookeeper', '~> 1.0.0'
11
+ # git 'git://github.com/slyphon/zookeeper.git', :tag => 'dev/zk/00006' do
12
+ # gem 'zookeeper', '~> 1.1.0'
13
13
  # end
14
14
 
15
-
16
15
  gem 'rake', :group => [:development, :test]
17
16
  gem 'pry', :group => [:development]
18
17
 
@@ -28,6 +27,13 @@ platform :mri_19 do
28
27
  gem 'simplecov', :group => :coverage, :require => false
29
28
  end
30
29
 
30
+ group :development do
31
+ gem 'guard', :require => false
32
+ gem 'guard-rspec', :require => false
33
+ gem 'guard-shell', :require => false
34
+ gem 'guard-bundler', :require => false
35
+ end
36
+
31
37
  group :test do
32
38
  gem 'rspec', '~> 2.8.0'
33
39
  gem 'flexmock', '~> 0.8.10'
@@ -0,0 +1,36 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ dot_rspec_path = File.expand_path('../.rspec', __FILE__)
5
+
6
+ rspec_options = File.open('.rspec', &:readlines).map(&:chomp).reject{|n| n =~ /\A(#|\Z)/}
7
+
8
+ guard 'bundler' do
9
+ watch 'Gemfile'
10
+ watch /\A.+\.gemspec\Z/
11
+ end
12
+
13
+ guard 'rspec', :version => 2 do
14
+ watch(%r{^spec/.+_spec\.rb$})
15
+
16
+ watch(%r%^spec/support/client_forker.rb$%) { 'spec/zk/00_forked_client_integration_spec.rb' }
17
+
18
+ watch(%r{^lib/(.+)\.rb$}) do |m|
19
+ case m[1]
20
+ when %r{^zk/event_handler$}
21
+ "spec/zk/watch_spec.rb"
22
+ when %r{^zk/client/threaded.rb$}
23
+ ["spec/zk/client_spec.rb", "spec/zk/zookeeper_spec.rb"]
24
+ when %r{^zk/locker/}
25
+ "spec/zk/locker_spec.rb"
26
+ when %r{^zk\.rb$}
27
+ 'spec' # run all tests
28
+ else
29
+ "spec/#{m[1]}_spec.rb"
30
+ end
31
+ end
32
+
33
+ watch('spec/spec_helper.rb') { "spec" }
34
+ end
35
+
36
+
@@ -65,6 +65,27 @@ In addition to all of that, I would like to think that the public API the ZK::Cl
65
65
  [zk-eventmachine]: https://github.com/slyphon/zk-eventmachine
66
66
 
67
67
  ## NEWS ##
68
+ ### v1.5.0 ###
69
+
70
+ Ok, now seriously this time. I think all of the forking issues are done.
71
+
72
+ * Implemented a 'stop the world' feature to ensure safety when forking. All threads are stopped, but state is preserved. `fork()` can then be called safely, and after fork returns, all threads will be restarted in the parent, and the connection will be torn down and reopened in the child.
73
+
74
+ * The easiest, and supported, way of doing this is now to call `ZK.install_fork_hook` after requiring zk. This will install an `alias_method_chain` style hook around the `Kernel.fork` method, which handles pausing all clients in the parent, calling fork, then resuming in the parent and reconnecting in the child. If you're using ZK in resque, I *highly* recommend using this approach, as it will give the most consistent results.
75
+
76
+ In your app that requires an open ZK instance and `fork()`:
77
+
78
+ ```ruby
79
+
80
+ require 'zk'
81
+ ZK.install_fork_hook
82
+
83
+ ```
84
+
85
+ Then use fork as you normally would.
86
+
87
+ * Logging is now off by default, but we now use the excellent, can't-recommend-it-enough, [logging](https://github.com/TwP/logging) gem. If you want to tap into the ZK logs, you can assign a stdlib compliant logger to `ZK.logger` and that will be used. Otherwise, you can use the Logging framework's controls. All ZK logs are consolidated under the 'ZK' logger instance.
88
+
68
89
 
69
90
  ### v1.4.1 ###
70
91
 
@@ -105,24 +126,6 @@ Phusion Passenger and Unicorn users are encouraged to upgrade!
105
126
 
106
127
  * See the fork-handling documentation [on the wiki](http://github.com/slyphon/zk/wiki/Forking).
107
128
 
108
- ### v1.2.0 ###
109
-
110
- You are __STRONGLY ENCOURAGED__ to go and look at the [CHANGELOG](http://git.io/tPbNBw) from the zookeeper 1.0.0 release
111
-
112
- * NOTICE: This release uses the 1.0 release of the zookeeper gem, which has had a MAJOR REFACTORING of its namespaces. Included in that zookeeper release is a compatibility layer that should ease the transition, but any references to Zookeeper\* heirarchy should be changed.
113
-
114
- * Refactoring related to the zokeeper gem, use all the new names internally now.
115
-
116
- * Create a new Subscription class that will be used as the basis for all subscription-type things.
117
-
118
- * Add new Locker features!
119
- * `LockerBase#assert!` - will raise an exception if the lock is not held. This check is not only for local in-memory "are we locked?" state, but will check the connection state and re-run the algorithmic tests that determine if a given Locker implementation actually has the lock.
120
- * `LockerBase#acquirable?` - an advisory method that checks if any condition would prevent the receiver from acquiring the lock.
121
-
122
- * Deprecation of the `lock!` and `unlock!` methods. These may change to be exception-raising in a future relase, so document and refactor that `lock` and `unlock` are the way to go.
123
-
124
- * Fixed a race condition in `event_catcher_spec.rb` that would cause 100% cpu usage and hang.
125
-
126
129
 
127
130
  ## Caveats
128
131
 
@@ -1,5 +1,15 @@
1
1
  This file notes feature differences and bugfixes contained between releases.
2
2
 
3
+ ### v1.5.0 ###
4
+
5
+ Ok, now seriously this time. I think all of the forking issues are done.
6
+
7
+ * Implemented a 'stop the world' feature to ensure safety when forking. All threads are stopped, but state is preserved. `fork()` can then be called safely, and after fork returns, all threads will be restarted in the parent, and the connection will be torn down and reopened in the child.
8
+
9
+ * The easiest, and supported, way of doing this is now to call `ZK.install_fork_hook` after requiring zk. This will install an `alias_method_chain` style hook around the `Kernel.fork` method, which handles pausing all clients in the parent, calling fork, then resuming in the parent and reconnecting in the child. If you're using ZK in resque, I *highly* recommend using this approach, as it will give the most consistent results.
10
+
11
+ * Logging is now off by default, and uses the excellent, can't-recommend-it-enough, [logging](https://github.com/TwP/logging) gem. If you want to tap into the ZK logs, you can assign a stdlib compliant logger to `ZK.logger` and that will be used. Otherwise, you can use the Logging framework's controls. All ZK logs are consolidated under the 'ZK' logger instance.
12
+
3
13
  ### v1.4.1 ###
4
14
 
5
15
  * True fork safety! The `zookeeper` at 1.1.0 is finally fork-safe. You can now use ZK in whatever forking library you want. Just remember to call `#reopen` on your client instance in the child process before attempting any opersations.
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ if File.exists?(release_ops_path)
8
8
  require File.join(release_ops_path, 'releaseops')
9
9
 
10
10
  # sets up the multi-ruby zk:test_all rake tasks
11
- ReleaseOps::TestTasks.define_for(*%w[1.8.7 1.9.2 jruby rbx ree 1.9.3])
11
+ ReleaseOps::TestTasks.define_for(*%w[1.8.7 1.9.2 jruby ree 1.9.3])
12
12
 
13
13
  # sets up the task :default => 'spec:run' and defines a simple
14
14
  # "run the specs with the current rvm profile" task
data/lib/zk.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
2
 
3
- require 'logger'
3
+ require 'logging'
4
4
  require 'zookeeper'
5
5
 
6
6
  # XXX: after 1.0 we'll need this
@@ -12,21 +12,28 @@ require 'monitor'
12
12
  require 'set'
13
13
  require 'time'
14
14
  require 'date'
15
+ require 'weakref'
15
16
 
16
17
  module ZK
18
+ # just like stdlib Monitor but provides the SAME API AS MUTEX, FFS!
19
+ # @private
20
+ class Monitor < Zookeeper::Monitor
21
+ end
17
22
  end
18
23
 
19
24
  require 'zk/core_ext'
20
- require 'zk/logging'
21
25
  require 'zk/exceptions'
22
26
  require 'zk/extensions'
27
+ require 'zk/logging'
23
28
  require 'zk/event'
24
29
  require 'zk/stat'
25
30
  require 'zk/subscription'
31
+ require 'zk/fork_hook'
26
32
  require 'zk/threadpool'
27
33
  require 'zk/threaded_callback'
28
34
  require 'zk/event_handler_subscription'
29
35
  require 'zk/event_handler'
36
+ require 'zk/node_deletion_watcher'
30
37
  require 'zk/message_queue'
31
38
  require 'zk/locker'
32
39
  require 'zk/election'
@@ -46,9 +53,7 @@ module ZK
46
53
  KILL_TOKEN = Object.new
47
54
  end
48
55
 
49
- unless @logger
50
- @logger = Logger.new($stderr).tap { |n| n.level = ENV['ZK_DEBUG'] ? Logger::DEBUG : Logger::ERROR }
51
- end
56
+ @@logger = nil unless defined? @@logger
52
57
 
53
58
  @default_host = 'localhost' unless @default_host
54
59
  @default_port = 2181 unless @default_port
@@ -65,14 +70,6 @@ module ZK
65
70
  attr_accessor :default_chroot
66
71
  end
67
72
 
68
- # @private
69
- def self.default_connection_string
70
- "#{default_host}:#{default_port}".tap do |str|
71
- # XXX: this is seriously blech
72
- str.replace(File.join(str, default_chroot)) if default_chroot != ''
73
- end
74
- end
75
-
76
73
  # The logger used by the ZK library. uses a Logger stderr with Logger::ERROR
77
74
  # level. The only thing that should ever be logged are exceptions that are
78
75
  # swallowed by background threads.
@@ -81,12 +78,20 @@ module ZK
81
78
  # implements the stdllb Logger API.
82
79
  #
83
80
  def self.logger
84
- @logger
81
+ @@logger
85
82
  end
86
83
 
87
- # Assign the Logger instance to be used by ZK
88
- def self.logger=(logger)
89
- @logger = logger
84
+ # set the ZK logger instance
85
+ def self.logger=(log)
86
+ @@logger = log
87
+ end
88
+
89
+ # @private
90
+ def self.default_connection_string
91
+ "#{default_host}:#{default_port}".tap do |str|
92
+ # XXX: this is seriously blech
93
+ str.replace(File.join(str, default_chroot)) if default_chroot != ''
94
+ end
90
95
  end
91
96
 
92
97
  # Create a new ZK::Client instance. If no arguments are given, the default
@@ -205,9 +210,10 @@ module ZK
205
210
  cnx = new(*args)
206
211
  yield cnx
207
212
  ensure
208
- cnx.close! if cnx
209
- # XXX: need some way of waiting for the connection to reach closed? state
210
- # ensure there's no leakage
213
+ if cnx
214
+ cnx.close!
215
+ Thread.pass until cnx.closed?
216
+ end
211
217
  end
212
218
 
213
219
  # creates a new ZK::Pool::Bounded with the default options.
@@ -292,7 +298,6 @@ module ZK
292
298
  open(host) do |zk| # do path stuff with the virgin connection
293
299
  unless zk.exists?(chroot_path) # someting must be done
294
300
  if chroot_opt == :create # here, let me...
295
- logger.debug { "creating chroot path #{chroot_path}" }
296
301
  zk.mkdir_p(chroot_path) # ...get that for you
297
302
  else # careful with that axe
298
303
  raise Exceptions::ChrootPathDoesNotExistError.new(host, chroot_path) # ...eugene
@@ -304,3 +309,5 @@ module ZK
304
309
  end
305
310
  end
306
311
 
312
+ ZK::Logging.set_default
313
+
@@ -140,8 +140,15 @@ module ZK
140
140
  @reconnect = opts.fetch(:reconnect, true)
141
141
 
142
142
  @mutex = Monitor.new
143
+ @cond = @mutex.new_cond
143
144
 
144
- @close_requested = false
145
+ @cli_state = :running # this is to distinguish between *our* state and the underlying connection state
146
+
147
+ @fork_subs = [
148
+ ForkHook.prepare_for_fork(method(:pause_before_fork_in_parent)),
149
+ ForkHook.after_fork_in_parent(method(:resume_after_fork_in_parent)),
150
+ ForkHook.after_fork_in_child(method(:reopen)),
151
+ ]
145
152
 
146
153
  yield self if block_given?
147
154
 
@@ -169,25 +176,69 @@ module ZK
169
176
  # ok, just to sanity check here
170
177
  raise "[BUG] we hit the fork-reopening code in JRuby!!" if defined?(::JRUBY_VERSION)
171
178
 
172
- logger.debug { "#{self.class}##{__method__} reopening everything, fork detected!" }
179
+ logger.debug { "reopening everything, fork detected!" }
173
180
 
174
181
  @mutex = Monitor.new
175
- @threadpool.reopen_after_fork! # prune dead threadpool threads after a fork()
176
- @event_handler.reopen_after_fork!
177
182
  @pid = Process.pid
183
+ @cli_state = :running # reset state to running if we were paused
178
184
 
179
185
  old_cnx, @cnx = @cnx, nil
180
186
  old_cnx.close! if old_cnx # && !old_cnx.closed?
187
+
188
+ @mutex.synchronize do
189
+ # it's important that we're holding the lock, as access to 'cnx' is
190
+ # synchronized, and we want to avoid a race where event handlers
191
+ # might see a nil connection. I've seen this exception occur *once*
192
+ # so it's pretty rare (it was on 1.8.7 too), but just to be double
193
+ # extra paranoid
194
+
195
+ @event_handler.reopen_after_fork!
196
+ @threadpool.reopen_after_fork! # prune dead threadpool threads after a fork()
197
+
198
+ connect
199
+ end
181
200
  else
182
- logger.debug { "#{self.class}##{__method__} not reopening, no fork detected" }
183
- @cnx.reopen(timeout)
201
+ @mutex.synchronize do
202
+ if @cli_state == :paused
203
+ # XXX: what to do in this case? does it matter?
204
+ end
205
+
206
+ logger.debug { "reopening, no fork detected" }
207
+ @cnx.reopen(timeout) # ok, we werent' forked, so just reopen
208
+ end
184
209
  end
185
210
 
186
- @mutex.synchronize { @close_requested = false }
187
- connect
188
211
  state
189
212
  end
190
213
 
214
+ # Before forking, call this method to peform a "stop the world" operation on all
215
+ # objects associated with this connection. This means that this client will spin down
216
+ # and join all threads (so make sure none of your callbacks will block forever),
217
+ # and will tke no action to keep the session alive. With the default settings,
218
+ # if a ping is not received within 20 seconds, the session is considered dead
219
+ # and must be re-established so be sure to call {#resume_after_fork_in_parent}
220
+ # before that deadline, or you will have to re-establish your session.
221
+ #
222
+ # @raise [InvalidStateError] when called and not in running? state
223
+ def pause_before_fork_in_parent
224
+ @mutex.synchronize do
225
+ raise InvalidStateError, "client must be running? when you call #{__method__}" unless running?
226
+ @cli_state = :paused
227
+ end
228
+ logger.debug { "#{self.class}##{__method__}" }
229
+ [@event_handler, @threadpool, @cnx].each(&:pause_before_fork_in_parent)
230
+ end
231
+
232
+ def resume_after_fork_in_parent
233
+ @mutex.synchronize do
234
+ raise InvalidStateError, "client must be paused? when you call #{__method__}" unless paused?
235
+ @cli_state = :running
236
+ end
237
+
238
+ logger.debug { "#{self.class}##{__method__}" }
239
+ [@cnx, @event_handler, @threadpool].each(&:resume_after_fork_in_parent)
240
+ end
241
+
191
242
  # (see Base#close!)
192
243
  #
193
244
  # @note We will make our best effort to do the right thing if you call
@@ -198,8 +249,9 @@ module ZK
198
249
  #
199
250
  def close!
200
251
  @mutex.synchronize do
201
- return if @close_requested
202
- @close_requested = true
252
+ return if [:closed, :close_requested].include?(@cli_state)
253
+ # logger.debug { "moving to :close_requested state" }
254
+ @cli_state = :close_requested
203
255
  end
204
256
 
205
257
  on_tpool = on_threadpool?
@@ -213,18 +265,24 @@ module ZK
213
265
  # and wait for it to exit
214
266
  #
215
267
  shutdown_thread = Thread.new do
216
- @threadpool.shutdown(2)
268
+ @threadpool.shutdown(10)
217
269
  super
218
- end
219
270
 
220
- shutdown_thread.join unless on_tpool
271
+ @mutex.synchronize do
272
+ # logger.debug { "moving to :closed state" }
273
+ @cli_state = :closed
274
+ end
275
+ end
221
276
 
222
- nil
277
+ on_tpool ? shutdown_thread : shutdown_thread.join
223
278
  end
224
279
 
225
280
  # {see Base#close}
226
281
  def close
227
282
  super
283
+ subs, @fork_subs = @fork_subs, []
284
+ subs.each(&:unsubscribe)
285
+ nil
228
286
  end
229
287
 
230
288
  # (see Threadpool#on_threadpool?)
@@ -245,12 +303,13 @@ module ZK
245
303
  return unless @reconnect
246
304
 
247
305
  @mutex.synchronize do
248
- unless @close_requested # a legitimate shutdown case
306
+ unless dead_or_dying? # a legitimate shutdown case
249
307
 
250
308
  logger.error { "Got event #{event.state_name}, calling reopen(0)! things may be messed up until this works itself out!" }
251
309
 
252
310
  # reopen(0) means that we don't want to wait for the connection
253
- # to reach the connected state before returning
311
+ # to reach the connected state before returning as we're on the
312
+ # event thread.
254
313
  reopen(0)
255
314
  end
256
315
  end
@@ -259,11 +318,42 @@ module ZK
259
318
  logger.error { "BUG: Exception caught in raw_event_handler: #{e.to_std_format}" }
260
319
  end
261
320
 
321
+ def closed?
322
+ return true if @mutex.synchronize { @cli_state == :closed }
323
+ super
324
+ end
325
+
326
+ # are we in running (not-paused) state?
327
+ def running?
328
+ @mutex.synchronize { @cli_state == :running }
329
+ end
330
+
331
+ # are we in paused state?
332
+ def paused?
333
+ @mutex.synchronize { @cli_state == :paused }
334
+ end
335
+
336
+ # has shutdown time arrived?
337
+ def close_requested?
338
+ @mutex.synchronize { @cli_state == :close_requested }
339
+ end
340
+
262
341
  protected
342
+ # in the threaded version of the client, synchronize access around cnx
343
+ # so that callers don't wind up with a nil object when we're in the middle
344
+ # of reopening it
345
+ def cnx
346
+ @mutex.synchronize { @cnx }
347
+ end
348
+
263
349
  # @private
264
350
  def create_connection(*args)
265
351
  ::Zookeeper.new(*args)
266
352
  end
267
- end
353
+
354
+ def dead_or_dying?
355
+ (@cli_state == :close_requested) || (@cli_state == :closed)
356
+ end
357
+ end
268
358
  end
269
359
  end