zk 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -8,3 +8,4 @@ html
8
8
  .rvmrc
9
9
  Gemfile.*
10
10
  wiki
11
+ zookeeper
@@ -3,15 +3,13 @@ notifications:
3
3
  email:
4
4
  - slyphon@gmail.com
5
5
 
6
+ env:
7
+ - SPAWN_ZOOKEEPER='true'
8
+
6
9
  rvm:
7
- - 1.8.7
8
- - 1.9.2
9
10
  - 1.9.3
10
- - jruby
11
-
12
- before_install:
13
- - sudo apt-get update
14
- - sudo apt-get install zookeeperd
11
+ - 1.9.2
12
+ - 1.8.7
15
13
 
16
- bundler_args: --without development
14
+ bundler_args: --without development docs
17
15
 
data/Gemfile CHANGED
@@ -2,11 +2,8 @@ source :rubygems
2
2
 
3
3
  # gem 'slyphon-zookeeper', :path => '~/zookeeper'
4
4
 
5
- group :development do
6
- gem 'rake'
7
- end
8
-
9
- gem 'pry', :group => [:development, :test]
5
+ gem 'rake', :group => [:development, :test]
6
+ gem 'pry', :group => [:development]
10
7
 
11
8
  group :docs do
12
9
  gem 'yard', '~> 0.7.5'
@@ -19,7 +16,8 @@ end
19
16
  group :test do
20
17
  gem 'rspec', '~> 2.8.0'
21
18
  gem 'flexmock', '~> 0.8.10'
22
- gem 'ZenTest', '~> 4.5.0'
19
+ # gem 'zk-server', :path => '~/mbox/zk-server'
20
+ gem 'zk-server', '~> 0.9.1'
23
21
  end
24
22
 
25
23
  # Specify your gem's dependencies in zk.gemspec
@@ -1,5 +1,7 @@
1
1
  # ZK
2
2
 
3
+ [![Build Status (master)](https://secure.travis-ci.org/slyphon/zk.png?branch=master)](http://travis-ci.org/slyphon/zk)
4
+
3
5
  ZK is a high-level interface to the Apache [ZooKeeper][] server. It is based on the [zookeeper gem][] which is a multi-Ruby low-level driver. Currently MRI 1.8.7, 1.9.2, 1.9.3, and JRuby are supported, rubinius 2.0.testing is supported-ish (it's expected to work, but upstream is unstable, so YMMV).
4
6
 
5
7
  ZK is licensed under the [MIT][] license.
@@ -16,7 +18,13 @@ Development is sponsored by [Snapfish][] and has been generously released to the
16
18
  [MIT]: http://www.gnu.org/licenses/license-list.html#Expat "MIT (Expat) License"
17
19
  [Snapfish]: http://www.snapfish.com/ "Snapfish"
18
20
 
19
- ## New in 1.0 !! ##
21
+ ## New in 1.1 !! ##
22
+
23
+ * NEW! Thread-per-Callback event delivery model! [Read all about it!](https://github.com/slyphon/zk/wiki/EventDeliveryModel). Provides a simple, sane way to increase the concurrency in your ZK-based app while maintaining the ordering guarantees ZooKeeper makes. Each callback can perform whatever work it needs to without blocking other callbacks from receiving events. Inspired by [Celluloid's](https://github.com/celluloid/celluloid) actor model.
24
+
25
+ * Use the [zk-server](https://github.com/slyphon/zk-server) gem to run a standalone ZooKeeper server for tests (`rake SPAWN_ZOOKEEPER=1`). Makes live-fire testing of any project that uses ZK easy to run anywhere!
26
+
27
+ ## New in 1.0 ##
20
28
 
21
29
  * Threaded client (the default one) will now automatically reconnect (i.e. `reopen()`) if a `SESSION_EXPIRED` or `AUTH_FAILED` event is received. Thanks to @eric for pointing out the _nose-on-your-face obviousness_ and importance of this. If users want to handle these events themselves, and not automatically reopen, you can pass `:reconnect => false` to the constructor.
22
30
 
@@ -24,39 +32,51 @@ Development is sponsored by [Snapfish][] and has been generously released to the
24
32
 
25
33
  * add zk.register(:all) to recevie node updates for all nodes (i.e. not filtered on path)
26
34
 
27
- * add 'interest' feature to zk.register, now you can indicate what kind of events should be delivered to the given block (previously you had to do that filtering inside the block). The default behavior is still the same, if no 'interest' is given, then all event types for the given path will be delivered to that block.
35
+ * add 'interest' feature to zk.register, now you can indicate what kind of events should be delivered to the given block (previously you had to do that filtering inside the block). The default behavior is still the same, if no 'interest' is given, then all event types for the given path will be delivered to that block.
28
36
 
29
- zk.register('/path', :created) do |event|
30
- # event.node_created? will always be true
31
- end
37
+ ```ruby
38
+ zk.register('/path', :created) do |event|
39
+ # event.node_created? will always be true
40
+ end
41
+
42
+ # or multiple kinds of events
32
43
 
33
- # or multiple kinds of events
44
+ zk.register('/path', [:created, :changed]) do |event|
45
+ # (event.node_created? or event.node_changed?) will always be true
46
+ end
34
47
 
35
- zk.register('/path', [:created, :changed]) do |event|
36
- # (event.node_created? or event.node_changed?) will always be true
37
- end
48
+ # this will, however, be changed in 1.1 to (backwards compatible, with a deprecation warning)
49
+
50
+ zk.register('/path', :only => :created) do |event|
51
+ end
52
+
53
+ ```
38
54
 
39
55
  * create now allows you to pass a path and options, instead of requiring the blank string
40
56
 
41
- zk.create('/path', '', :sequential => true)
57
+ ```ruby
58
+ zk.create('/path', '', :sequential => true)
42
59
 
43
- # now also
60
+ # now also
44
61
 
45
- zk.create('/path', :sequential => true)
62
+ zk.create('/path', :sequential => true)
63
+ ```
46
64
 
47
65
  * fix for shutdown: close! called from threadpool will do the right thing
48
66
 
49
67
  * Chroot users rejoice! By default, ZK.new will create a chrooted path for you.
50
68
 
51
- ZK.new('localhost:2181/path', :chroot => :create) # the default, create the path before returning connection
69
+ ```ruby
70
+ ZK.new('localhost:2181/path', :chroot => :create) # the default, create the path before returning connection
52
71
 
53
- ZK.new('localhost:2181/path', :chroot => :check) # make sure the chroot exists, raise if not
72
+ ZK.new('localhost:2181/path', :chroot => :check) # make sure the chroot exists, raise if not
54
73
 
55
- ZK.new('localhost:2181/path', :chroot => :do_nothing) # old default behavior
74
+ ZK.new('localhost:2181/path', :chroot => :do_nothing) # old default behavior
56
75
 
57
- # and, just for kicks
58
-
59
- ZK.new('localhost:2181', :chroot => '/path') # equivalent to 'localhost:2181/path', :chroot => :create
76
+ # and, just for kicks
77
+
78
+ ZK.new('localhost:2181', :chroot => '/path') # equivalent to 'localhost:2181/path', :chroot => :create
79
+ ```
60
80
 
61
81
  * Most of the event functionality used is now in a ZK::Event module. This is still mixed into the underlying slyphon-zookeeper class, but now all of the important and relevant methods are documented, and Event appears as a first-class citizen.
62
82
 
@@ -115,8 +135,6 @@ ZK strives to be a complete, correct, and convenient way of interacting with Zoo
115
135
 
116
136
  * ZK::Client supports asynchronous calls of all basic methods (get, set, delete, etc.) however these versions are kind of inconvenient to use. For a fully evented stack, try [zk-eventmachine][], which is designed to be compatible and convenient to use in event-driven code.
117
137
 
118
- * ZooKeeper "chroot" [connection syntax][chroot] should work for most cases. Right now we require that the root path exist before the chrooted client is used, but that may change [in the near future](https://github.com/slyphon/zk/issues/7).
119
-
120
138
  [twitter/zookeeper]: https://github.com/twitter/zookeeper
121
139
  [async-branch]: https://github.com/slyphon/zk/tree/dev%2Fasync-conveniences
122
140
  [chroot]: http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#ch_zkSessions
@@ -133,7 +151,7 @@ ZK strives to be a complete, correct, and convenient way of interacting with Zoo
133
151
 
134
152
  ## Dependencies
135
153
 
136
- * The [slyphon-zookeeper gem][szk-gem] ([repo][szk-repo]), which adds JRuby compatibility and a full suite of tests to the excellent [twitter/zookeeper][] project.
154
+ * The [slyphon-zookeeper gem][szk-gem] ([repo][szk-repo]).
137
155
 
138
156
  * For JRuby, the [slyphon-zookeeper\_jar gem][szk-jar-gem] ([repo][szk-jar-repo]), which just wraps the upstream zookeeper driver jar in a gem for easy installation
139
157
 
data/Rakefile CHANGED
@@ -33,7 +33,7 @@ GEMSPEC_NAME = 'zk.gemspec'
33
33
  end
34
34
 
35
35
  task rspec_task_name => bundle_task_name do
36
- sh "rvm #{ruby_with_gemset} do env BUNDLE_GEMFILE=#{phony_gemfile_link_name} bundle exec rspec spec --fail-fast"
36
+ sh "rvm #{ruby_with_gemset} do env JRUBY_OPTS='--1.9' BUNDLE_GEMFILE=#{phony_gemfile_link_name} bundle exec rspec spec --fail-fast"
37
37
  end
38
38
 
39
39
  task "mb:#{ns_name}" => rspec_task_name
@@ -69,7 +69,9 @@ namespace :spec do
69
69
  require 'bundler/setup'
70
70
  require 'rspec/core/rake_task'
71
71
 
72
- RSpec::Core::RakeTask.new('spec:runner')
72
+ RSpec::Core::RakeTask.new('spec:runner') do |t|
73
+ t.rspec_opts = '-f d'
74
+ end
73
75
  end
74
76
 
75
77
  task :run => :define do
data/lib/zk.rb CHANGED
@@ -16,6 +16,7 @@ require 'zk/extensions'
16
16
  require 'zk/event'
17
17
  require 'zk/stat'
18
18
  require 'zk/threadpool'
19
+ require 'zk/threaded_callback'
19
20
  require 'zk/event_handler_subscription'
20
21
  require 'zk/event_handler'
21
22
  require 'zk/message_queue'
@@ -30,9 +31,6 @@ module ZK
30
31
  silence_warnings do
31
32
  # @private
32
33
  ZK_ROOT = File.expand_path('../..', __FILE__).freeze
33
-
34
- # @private
35
- DEFAULT_SERVER = 'localhost:2181'.freeze
36
34
  end
37
35
 
38
36
  unless defined?(KILL_TOKEN)
@@ -40,10 +38,32 @@ module ZK
40
38
  KILL_TOKEN = Object.new
41
39
  end
42
40
 
43
-
44
41
  unless @logger
45
42
  @logger = Logger.new($stderr).tap { |n| n.level = Logger::ERROR }
46
43
  end
44
+
45
+ @default_host = 'localhost' unless @default_host
46
+ @default_port = 2181 unless @default_port
47
+ @default_chroot = '' unless @default_chroot
48
+
49
+ class << self
50
+ # what host should ZK.new connect to when given no options
51
+ attr_accessor :default_host
52
+
53
+ # what port should ZK.new connect to when given no options
54
+ attr_accessor :default_port
55
+
56
+ # what chroot path should ZK.new connect to when given no options
57
+ attr_accessor :default_chroot
58
+ end
59
+
60
+ # @private
61
+ def self.default_connection_string
62
+ "#{default_host}:#{default_port}".tap do |str|
63
+ # XXX: this is seriously blech
64
+ str.replace(File.join(str, default_chroot)) if default_chroot != ''
65
+ end
66
+ end
47
67
 
48
68
  # The logger used by the ZK library. uses a Logger stderr with Logger::ERROR
49
69
  # level. The only thing that should ever be logged are exceptions that are
@@ -139,18 +159,22 @@ module ZK
139
159
  # {ZK::Client::Threaded#initialize Threaded.new} directly. You probably
140
160
  # also hate happiness and laughter.
141
161
  #
162
+ # @option opts [:single,:per_callback] :thread (:single) see {ZK::Client::Threaded#initialize}
163
+ # for a discussion of what these options mean
164
+ #
142
165
  # @raise [ChrootPathDoesNotExistError] if a chroot path is specified,
143
166
  # `:chroot` is `:check`, and the path does not exist.
144
167
  #
145
168
  # @raise [ArgumentError] if both a chrooted `connection_str` is given *and* a
146
169
  # `String` value for the `:chroot` option is given
147
170
  #
171
+ #
148
172
  def self.new(*args, &block)
149
173
  opts = args.extract_options!
150
174
 
151
175
  chroot_opt = opts.fetch(:chroot, :create)
152
176
 
153
- args = [DEFAULT_SERVER] if args.empty? # the ZK.new() case
177
+ args = [default_connection_string] if args.empty? # the ZK.new() case
154
178
 
155
179
  if args.first.kind_of?(String)
156
180
  if new_cnx_str = do_chroot_setup(args.first, chroot_opt)
@@ -174,6 +198,8 @@ module ZK
174
198
  yield cnx
175
199
  ensure
176
200
  cnx.close! if cnx
201
+ # XXX: need some way of waiting for the connection to reach closed? state
202
+ # ensure there's no leakage
177
203
  end
178
204
 
179
205
  # creates a new ZK::Pool::Bounded with the default options.
@@ -186,6 +212,34 @@ module ZK
186
212
  File.join(*paths)
187
213
  end
188
214
 
215
+ # @private
216
+ def self.mri_19?
217
+ ruby_19? and not jruby? or rubinius?
218
+ end
219
+
220
+ def self.ruby_19?
221
+ (RUBY_VERSION =~ /\A1\.9\.[2-9]\Z/)
222
+ end
223
+
224
+ def self.ruby_187?
225
+ (RUBY_VERSION == '1.8.7')
226
+ end
227
+
228
+ # @private
229
+ def self.mri_187?
230
+ ruby_187? and not jruby? or rubinius?
231
+ end
232
+
233
+ # @private
234
+ def self.jruby?
235
+ defined?(::JRUBY_VERSION)
236
+ end
237
+
238
+ # @private
239
+ def self.rubinius?
240
+ defined?(::Rubinius)
241
+ end
242
+
189
243
  private
190
244
  # @return [String] a possibly modified connection string (with chroot info
191
245
  # added)
@@ -226,6 +280,7 @@ module ZK
226
280
  open(host) do |zk| # do path stuff with the virgin connection
227
281
  unless zk.exists?(chroot_path) # someting must be done
228
282
  if chroot_opt == :create # here, let me...
283
+ logger.debug { "creating chroot path #{chroot_path}" }
229
284
  zk.mkdir_p(chroot_path) # ...get that for you
230
285
  else # careful with that axe
231
286
  raise Exceptions::ChrootPathDoesNotExistError.new(host, chroot_path) # ...eugene
@@ -770,12 +770,22 @@ module ZK
770
770
  #
771
771
  # @example only creation events
772
772
  #
773
- # sub = zk.register('/path/to/znode', :created) do |event|
773
+ # sub = zk.register('/path/to/znode', :only => :created) do |event|
774
774
  # # do something when the node is created
775
775
  # end
776
776
  #
777
777
  # @example only changed or children events
778
778
  #
779
+ # sub = zk.register('/path/to/znode', :only => [:changed, :child]) do |event|
780
+ # if event.node_changed?
781
+ # # do something on change
782
+ # else
783
+ # # we know it's a child event
784
+ # end
785
+ # end
786
+ #
787
+ # @example deprecated 1.0 style interests
788
+ #
779
789
  # sub = zk.register('/path/to/znode', [:changed, :child]) do |event|
780
790
  # if event.node_changed?
781
791
  # # do something on change
@@ -787,12 +797,6 @@ module ZK
787
797
  # @param [String,:all] path the znode path you want to listen to, or the
788
798
  # special value :all, that will cause the block to be delivered events
789
799
  # for all znode paths
790
- #
791
- # @param [Array,Symbol,nil] interests a symbol or array-of-symbols indicating
792
- # which events you would like the block to be called for. Valid events
793
- # are :created, :deleted, :changed, and :child. If nil, the block will
794
- # receive all events
795
- #
796
800
  # @param [Block] block the block to execute when a watch event happpens
797
801
  #
798
802
  # @yield [event] We will call your block with the watch event object (which
@@ -801,12 +805,30 @@ module ZK
801
805
  # @return [EventHandlerSubscription] the subscription object
802
806
  # you can use to to unsubscribe from an event
803
807
  #
808
+ # @overload register(path, interests=nil, &block)
809
+ # @since 1.0
810
+ #
811
+ # @deprecated use the `:only => :created` form
812
+ #
813
+ # @param [Array,Symbol,nil] interests a symbol or array-of-symbols indicating
814
+ # which events you would like the block to be called for. Valid events
815
+ # are :created, :deleted, :changed, and :child. If nil, the block will
816
+ # receive all events
817
+ #
818
+ # @overload register(path, opts={}, &block)
819
+ # @since 1.1
820
+ #
821
+ # @option opts [Array,Symbol,nil] :only (nil) a symbol or array-of-symbols indicating
822
+ # which events you would like the block to be called for. Valid events
823
+ # are :created, :deleted, :changed, and :child. If nil, the block will
824
+ # receive all events
825
+ #
804
826
  # @see ZooKeeper::WatcherEvent
805
827
  # @see ZK::EventHandlerSubscription
806
828
  # @see https://github.com/slyphon/zk/wiki/Events the wiki page on using events effectively
807
829
  #
808
- def register(path, interests=nil, &block)
809
- event_handler.register(path, interests, &block)
830
+ def register(path, opts={}, &block)
831
+ event_handler.register(path, opts, &block)
810
832
  end
811
833
 
812
834
  # returns true if the caller is calling from the event dispatch thread
@@ -15,23 +15,11 @@ module ZK
15
15
  # unconfigurable, event dispatch thread. In 1.0 the number of event
16
16
  # delivery threads is configurable, but still defaults to 1.
17
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
18
  # If you use the threadpool/event callbacks to perform work, you may be
28
19
  # interested in registering an `on_exception` callback that will receive
29
20
  # all exceptions that occur on the threadpool that are not handled (i.e.
30
21
  # that bubble up to top of a block).
31
22
  #
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
23
  #
36
24
  # @example Register on_connected callback.
37
25
  #
@@ -67,8 +55,18 @@ module ZK
67
55
  # @note The documentation for 0.9.0 was incorrect in stating the number
68
56
  # of threads used to deliver events. There was one, unconfigurable,
69
57
  # 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).
58
+ # configurable, but still defaults to 1 and users are discouraged from
59
+ # adjusting the value due to the complexity this introduces. In 1.1
60
+ # there is a better option for achieving higher concurrency (see the
61
+ # `:thread` option)
62
+ #
63
+ # The Management apologizes for any confusion this may have caused.
64
+ #
65
+ # @since __1.1__: Instead of adjusting the threadpool, users are _strongly_ encouraged
66
+ # to use the `:thread => :per_callback` option to increase the
67
+ # parallelism of event delivery safely and sanely. Please see
68
+ # [this wiki article](https://github.com/slyphon/zk/wiki/EventDeliveryModel) for more
69
+ # information and a demonstration.
72
70
  #
73
71
  # @param [String] host (see ZK::Client::Base#initialize)
74
72
  #
@@ -77,13 +75,34 @@ module ZK
77
75
  # case of an expired session, we will keep trying to reestablish the
78
76
  # connection.
79
77
  #
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.
78
+ # @option opts [:single,:per_callback] :thread (:single) choose your event
79
+ # delivery model:
80
+ #
81
+ # * `:single`: There is one thread, and only one callback is called at
82
+ # a time. This is the default mode (for now), and will provide the most
83
+ # safety for your app. All events will be delivered as received, to
84
+ # callbacks in the order they were registered. This safety has the
85
+ # tradeoff that if one of your callbacks performs some action that blocks
86
+ # the delivery thread, you will not recieve other events until it returns.
87
+ # You're also limiting the concurrency of your app. This should be fine
88
+ # for most simple apps, and is a good choice to start with when
89
+ # developing your application
90
+ #
91
+ # * `:per_callback`: This option will use a new-style Actor model (inspired by
92
+ # [Celluloid](https://github.com/celluloid/celluloid)) that uses a
93
+ # per-callback queue and thread to allow for greater concurrency in
94
+ # your app, whille still maintaining some kind of sanity. By choosing
95
+ # this option your callbacks will receive events in order, and will
96
+ # receive only one at a time, but in parallel with other callbacks.
97
+ # This model has the advantage you can have all of your callbacks
98
+ # making progress in parallel, and if one of them happens to block,
99
+ # it will not affect the others.
100
+ #
101
+ # * see {https://github.com/slyphon/zk/wiki/EventDeliveryModel the wiki} for a
102
+ # discussion and demonstration of the effect of this setting.
84
103
  #
85
104
  # @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
105
+ # to be established. If timeout is nil, we will wait forever: *use
87
106
  # carefully*.
88
107
  #
89
108
  # @yield [self] calls the block with the new instance after the event
@@ -101,7 +120,7 @@ module ZK
101
120
  @threadpool = Threadpool.new(tp_size)
102
121
 
103
122
  @session_timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT) # maybe move this into superclass?
104
- @event_handler = EventHandler.new(self)
123
+ @event_handler = EventHandler.new(self, opts)
105
124
 
106
125
  @reconnect = opts.fetch(:reconnect, true)
107
126