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
@@ -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