zk 0.9.1 → 1.0.0.rc.1

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 (63) hide show
  1. data/.gitignore +2 -2
  2. data/Gemfile +3 -4
  3. data/README.markdown +14 -5
  4. data/RELEASES.markdown +69 -1
  5. data/Rakefile +50 -18
  6. data/docs/examples/block_until_node_deleted_ex.rb +63 -0
  7. data/docs/examples/events_01.rb +36 -0
  8. data/docs/examples/events_02.rb +41 -0
  9. data/lib/{z_k → zk}/client/base.rb +87 -30
  10. data/lib/{z_k → zk}/client/conveniences.rb +0 -1
  11. data/lib/{z_k → zk}/client/state_mixin.rb +0 -0
  12. data/lib/zk/client/threaded.rb +196 -0
  13. data/lib/{z_k → zk}/client/unixisms.rb +0 -0
  14. data/lib/zk/client.rb +59 -0
  15. data/lib/zk/core_ext.rb +75 -0
  16. data/lib/{z_k → zk}/election.rb +0 -0
  17. data/lib/zk/event.rb +168 -0
  18. data/lib/{z_k → zk}/event_handler.rb +53 -28
  19. data/lib/zk/event_handler_subscription.rb +68 -0
  20. data/lib/{z_k → zk}/exceptions.rb +38 -23
  21. data/lib/zk/extensions.rb +79 -0
  22. data/lib/{z_k → zk}/find.rb +0 -0
  23. data/lib/{z_k → zk}/locker.rb +0 -0
  24. data/lib/{z_k → zk}/logging.rb +0 -0
  25. data/lib/{z_k → zk}/message_queue.rb +8 -4
  26. data/lib/{z_k → zk}/mongoid.rb +0 -0
  27. data/lib/{z_k → zk}/pool.rb +0 -0
  28. data/lib/zk/stat.rb +115 -0
  29. data/lib/{z_k → zk}/threadpool.rb +52 -4
  30. data/lib/zk/version.rb +3 -0
  31. data/lib/zk.rb +238 -1
  32. data/spec/message_queue_spec.rb +2 -2
  33. data/spec/shared/client_contexts.rb +8 -20
  34. data/spec/shared/client_examples.rb +136 -2
  35. data/spec/spec_helper.rb +4 -2
  36. data/spec/support/event_catcher.rb +11 -0
  37. data/spec/support/exist_matcher.rb +6 -0
  38. data/spec/support/logging.rb +2 -1
  39. data/spec/watch_spec.rb +194 -10
  40. data/spec/{z_k → zk}/client/locking_and_session_death_spec.rb +0 -32
  41. data/spec/zk/client_spec.rb +23 -0
  42. data/spec/{z_k → zk}/election_spec.rb +0 -0
  43. data/spec/{z_k → zk}/extensions_spec.rb +0 -0
  44. data/spec/{z_k → zk}/locker_spec.rb +0 -40
  45. data/spec/zk/module_spec.rb +185 -0
  46. data/spec/{z_k → zk}/mongoid_spec.rb +0 -2
  47. data/spec/{z_k → zk}/pool_spec.rb +0 -2
  48. data/spec/{z_k → zk}/threadpool_spec.rb +32 -4
  49. data/spec/zookeeper_spec.rb +1 -6
  50. data/zk.gemspec +2 -2
  51. metadata +64 -56
  52. data/lib/z_k/client/continuation_proxy.rb +0 -109
  53. data/lib/z_k/client/drop_box.rb +0 -98
  54. data/lib/z_k/client/multiplexed.rb +0 -28
  55. data/lib/z_k/client/threaded.rb +0 -76
  56. data/lib/z_k/client.rb +0 -35
  57. data/lib/z_k/event_handler_subscription.rb +0 -36
  58. data/lib/z_k/extensions.rb +0 -155
  59. data/lib/z_k/version.rb +0 -3
  60. data/lib/z_k.rb +0 -97
  61. data/spec/z_k/client/drop_box_spec.rb +0 -90
  62. data/spec/z_k/client/multiplexed_spec.rb +0 -20
  63. data/spec/z_k/client_spec.rb +0 -7
data/lib/zk.rb CHANGED
@@ -1,2 +1,239 @@
1
- require File.expand_path('../z_k', __FILE__)
1
+ require 'rubygems'
2
+
3
+ require 'logger'
4
+ require 'zookeeper'
5
+ require 'forwardable'
6
+ require 'thread'
7
+ require 'monitor'
8
+ require 'set'
9
+ require 'time'
10
+ require 'date'
11
+
12
+ require 'zk/core_ext'
13
+ require 'zk/logging'
14
+ require 'zk/exceptions'
15
+ require 'zk/extensions'
16
+ require 'zk/event'
17
+ require 'zk/stat'
18
+ require 'zk/threadpool'
19
+ require 'zk/event_handler_subscription'
20
+ require 'zk/event_handler'
21
+ require 'zk/message_queue'
22
+ require 'zk/locker'
23
+ require 'zk/election'
24
+ require 'zk/mongoid'
25
+ require 'zk/client'
26
+ require 'zk/pool'
27
+ require 'zk/find'
28
+
29
+ module ZK
30
+ silence_warnings do
31
+ # @private
32
+ ZK_ROOT = File.expand_path('../..', __FILE__).freeze
33
+
34
+ # @private
35
+ DEFAULT_SERVER = 'localhost:2181'.freeze
36
+ end
37
+
38
+ unless defined?(KILL_TOKEN)
39
+ # @private
40
+ KILL_TOKEN = Object.new
41
+ end
42
+
43
+
44
+ unless @logger
45
+ @logger = Logger.new($stderr).tap { |n| n.level = Logger::ERROR }
46
+ end
47
+
48
+ # The logger used by the ZK library. uses a Logger stderr with Logger::ERROR
49
+ # level. The only thing that should ever be logged are exceptions that are
50
+ # swallowed by background threads.
51
+ #
52
+ # You can change this logger by setting ZK#logger= to an object that
53
+ # implements the stdllb Logger API.
54
+ #
55
+ def self.logger
56
+ @logger
57
+ end
58
+
59
+ # Assign the Logger instance to be used by ZK
60
+ def self.logger=(logger)
61
+ @logger = logger
62
+ end
63
+
64
+ # Create a new ZK::Client instance. If no arguments are given, the default
65
+ # config of `localhost:2181` will be used. Otherwise all args will be passed
66
+ # to ZK::Client#new
67
+ #
68
+ # if a block is given, it will be yielded the client *before* the connection
69
+ # is established, this is useful for registering connected-state handlers.
70
+ #
71
+ # Since 1.0, if you pass a chrooted host string, i.e. `localhost:2181/foo/bar/baz` this
72
+ # method will create two connections. The first will be short lived, and will create the
73
+ # chroot path, the second will be the chrooted one and returned to the user. This is
74
+ # meant as a convenience to users who want to use chrooted connections.
75
+ #
76
+ # @note As it says in the ZooKeeper [documentation](http://zookeeper.apache.org/doc/r3.4.3/zookeeperProgrammers.html#ch_gotchas),
77
+ # if you are running a cluster: "The list of ZooKeeper servers used by the
78
+ # client must match the list of ZooKeeper servers that each ZooKeeper
79
+ # server has. Things can work, although not optimally, if the client list
80
+ # is a subset of the real list of ZooKeeper servers, but not if the client
81
+ # lists ZooKeeper servers not in the ZooKeeper cluster."
82
+ #
83
+ # @example Connection using defaults
84
+ #
85
+ # zk = ZK.new # will connect to 'localhost:2181'
86
+ #
87
+ # @example Connection to a single server
88
+ #
89
+ # zk = ZK.new('localhost:2181')
90
+ #
91
+ # @example Connection to a single server with a chroot (automatically created)
92
+ #
93
+ # zk = ZK.new('localhost:2181/look/around/you')
94
+ #
95
+ # @example Connection to multiple servers (a cluster)
96
+ #
97
+ # zk = ZK.new('server1:2181,server2:2181,server3:2181')
98
+ #
99
+ # @example Connection to multiple servers with a chroot (chroot will automatically be creatd)
100
+ #
101
+ # zk = ZK.new('server1:2181,server2:2181,server3:2181/look/around/you')
102
+ #
103
+ # @example Connection to a single server, assert that chroot path exists, but do not create it
104
+ #
105
+ # zk = ZK.new('localhost:2181/look/around/you', :chroot => :check)
106
+ #
107
+ # @example Connection to a single server, use a chrooted connection, do not check for validity, do not create
108
+ #
109
+ # zk = ZK.new('localhost:2181/look/around/you', :chroot => :do_nothing)
110
+ #
111
+ # @example Connection to a single server, chroot path specified as an option
112
+ #
113
+ # zk = ZK.new('localhost:2181', :chroot => '/look/around/you')
114
+ #
115
+ # @overload new(connection_str, opts={}, &block)
116
+ # @param [String] connection_str A zookeeper host connection string, which
117
+ # is a comma-separated list of zookeeper servers and an optional chroot
118
+ # path.
119
+ #
120
+ # @option opts [:create,:check,:do_nothing,String] :chroot (:create) if a chrooted
121
+ # `connection_str`, `:chroot` can have the following values:
122
+ #
123
+ # * `:create` (the default), then we will use a secondary (short-lived)
124
+ # un-chrooted connection to ensure that the path exists before returning
125
+ # the chrooted connection.
126
+ #
127
+ # * `:check`, we will not attempt to create the connection, but rather
128
+ # will raise a {Exceptions::ChrootPathDoesNotExistError
129
+ # ChrootPathDoesNotExistError} if the path doesn't exist.
130
+ #
131
+ # * `:do_nothing`, we do not create the path and furthermore we do not
132
+ # perform the check (the `<= 0.9` behavior).
133
+ #
134
+ # * if a `String` is given, it is used as the chroot path, and we will follow
135
+ # the same rules as if `:create` was given if `connection_str` also
136
+ # contains a chroot path, we raise an `ArgumentError`
137
+ #
138
+ # * if you don't like this for some reason, you can always use
139
+ # {ZK::Client::Threaded#initialize Threaded.new} directly. You probably
140
+ # also hate happiness and laughter.
141
+ #
142
+ # @raise [ChrootPathDoesNotExistError] if a chroot path is specified,
143
+ # `:chroot` is `:check`, and the path does not exist.
144
+ #
145
+ # @raise [ArgumentError] if both a chrooted `connection_str` is given *and* a
146
+ # `String` value for the `:chroot` option is given
147
+ #
148
+ def self.new(*args, &block)
149
+ opts = args.extract_options!
150
+
151
+ chroot_opt = opts.fetch(:chroot, :create)
152
+
153
+ args = [DEFAULT_SERVER] if args.empty? # the ZK.new() case
154
+
155
+ if args.first.kind_of?(String)
156
+ if new_cnx_str = do_chroot_setup(args.first, chroot_opt)
157
+ args[0] = new_cnx_str
158
+ end
159
+ else
160
+ raise ArgumentError, "cannot create a connection given args array: #{args}"
161
+ end
162
+
163
+ opts.delete(:chroot_opt)
164
+
165
+ args << opts
166
+
167
+ Client.new(*args, &block)
168
+ end
169
+
170
+ # Like new, yields a connection to the given block and closes it when the
171
+ # block returns
172
+ def self.open(*args)
173
+ cnx = new(*args)
174
+ yield cnx
175
+ ensure
176
+ cnx.close! if cnx
177
+ end
178
+
179
+ # creates a new ZK::Pool::Bounded with the default options.
180
+ def self.new_pool(host, opts={})
181
+ ZK::Pool::Bounded.new(host, opts)
182
+ end
183
+
184
+ # @private
185
+ def self.join(*paths)
186
+ File.join(*paths)
187
+ end
188
+
189
+ private
190
+ # @return [String] a possibly modified connection string (with chroot info
191
+ # added)
192
+ #
193
+ def self.do_chroot_setup(cnx_str, chroot_opt=:create)
194
+ # "it should set up the chroot for us," they says.
195
+ # "it's confusing if it doesn't do that for us," they says.
196
+ # sheesh, look at this...
197
+
198
+ host, chroot_path = Client.split_chroot(cnx_str)
199
+
200
+ case chroot_opt
201
+ when :do_nothing
202
+ return
203
+ when String
204
+ if chroot_path
205
+ raise ArgumentError, "You cannot give a connection_str with a chroot path (#{cnx_str}) *and* specify a :chroot => #{chroot_opt} too!"
206
+ else
207
+ # ok, cnx_str didn't have a chroot path on it, but the user
208
+ # specified :chroot => '/path'. we'll use that, then
209
+ chroot_path = chroot_opt.dup
210
+ chroot_opt = :create
211
+
212
+ # oh, and return the correct string later
213
+ cnx_str = "#{host}#{chroot_path}"
214
+ end
215
+ when :create, :check
216
+ # no-op, valid options for later
217
+ else
218
+ raise ArgumentError, ":chroot must be one of :create, :check, :do_nothing, or a String, not: #{chroot_opt.inspect}"
219
+ end
220
+
221
+ return cnx_str unless chroot_path # if by this point, we don't have a chroot_path, then there isn't one to be had
222
+
223
+ # make sure the given path is kosher
224
+ Client.assert_valid_chroot_str!(chroot_path)
225
+
226
+ open(host) do |zk| # do path stuff with the virgin connection
227
+ unless zk.exists?(chroot_path) # someting must be done
228
+ if chroot_opt == :create # here, let me...
229
+ zk.mkdir_p(chroot_path) # ...get that for you
230
+ else # careful with that axe
231
+ raise Exceptions::ChrootPathDoesNotExistError.new(host, chroot_path) # ...eugene
232
+ end
233
+ end
234
+ end
235
+
236
+ cnx_str # the possibly-modified connection string (with chroot info)
237
+ end
238
+ end
2
239
 
@@ -3,8 +3,8 @@ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
3
  describe ZK::MessageQueue do
4
4
 
5
5
  before(:each) do
6
- @zk = ZK.new("localhost:#{ZK_TEST_PORT}", :watcher => :default)
7
- @zk2 = ZK.new("localhost:#{ZK_TEST_PORT}", :watcher => :default)
6
+ @zk = ZK.new("localhost:#{ZK_TEST_PORT}")
7
+ @zk2 = ZK.new("localhost:#{ZK_TEST_PORT}")
8
8
  wait_until{ @zk.connected? && @zk2.connected? }
9
9
  @queue_name = "_specQueue"
10
10
  @consume_queue = @zk.queue(@queue_name)
@@ -3,30 +3,18 @@ shared_context 'threaded client connection' do
3
3
  @connection_string = "localhost:#{ZK_TEST_PORT}"
4
4
  @base_path = '/zktests'
5
5
  @zk = ZK::Client::Threaded.new(@connection_string).tap { |z| wait_until { z.connected? } }
6
+ @zk.on_exception { |e| raise e }
6
7
  @zk.rm_rf(@base_path)
7
8
  end
8
9
 
9
10
  after do
10
- @zk.rm_rf(@base_path)
11
- @zk.close!
12
-
13
- wait_until(2) { @zk.closed? }
14
- end
15
- end
16
-
17
- shared_context 'multiplexed client connection' do
18
- before do
19
- @connection_string = "localhost:#{ZK_TEST_PORT}"
20
- @base_path = '/zktests'
21
- @zk = ZK::Client::Multiplexed.new(@connection_string).tap { |z| wait_until { z.connected? } }
22
- @zk.rm_rf(@base_path)
23
- end
24
-
25
- after do
26
- @zk.rm_rf(@base_path)
27
- @zk.close!
28
-
29
- wait_until(2) { @zk.closed? }
11
+ if @zk.closed?
12
+ ZK.open(@connection_string) { |z| z.rm_rf(@base_path) }
13
+ else
14
+ @zk.rm_rf(@base_path)
15
+ @zk.close!
16
+ wait_until(2) { @zk.closed? }
17
+ end
30
18
  end
31
19
  end
32
20
 
@@ -3,6 +3,7 @@ shared_examples_for 'client' do
3
3
  before(:each) do
4
4
  @path_ary = %w[test mkdir_p path creation]
5
5
  @bogus_path = File.join('/', *@path_ary)
6
+ @zk.rm_rf('/test')
6
7
  end
7
8
 
8
9
  it %[should create all intermediate paths for the path givem] do
@@ -14,6 +15,95 @@ shared_examples_for 'client' do
14
15
  end
15
16
  end
16
17
 
18
+ # nail down all possible cases
19
+ describe :create do
20
+ describe 'only path given' do
21
+ it %[should create a node with blank data] do
22
+ @zk.create(@base_path)
23
+ @zk.get(@base_path).first.should == ''
24
+ end
25
+ end
26
+
27
+ describe 'path and data given' do
28
+ it %[should create a node with the path and data] do
29
+ @zk.create(@base_path, 'blah')
30
+ @zk.get(@base_path).first.should == 'blah'
31
+ end
32
+ end
33
+
34
+ describe 'path and sequential' do
35
+ it %[should create a sequential node with blank data] do
36
+ @zk.create(@base_path)
37
+ path = @zk.create("#{@base_path}/v", :sequential => true)
38
+ path.start_with?(@base_path).should be_true
39
+
40
+ File.basename(path).should match(/v\d+/)
41
+
42
+ @zk.get(path).first.should == ''
43
+ end
44
+
45
+ it %[should create a sequential node with given data] do
46
+ @zk.create(@base_path)
47
+ path = @zk.create("#{@base_path}/v", 'thedata', :sequential => true)
48
+ path.start_with?(@base_path).should be_true
49
+
50
+ File.basename(path).should match(/v\d+/)
51
+
52
+ data, st = @zk.get(path)
53
+ data.should == 'thedata'
54
+ st.should_not be_ephemeral
55
+ end
56
+ end
57
+
58
+ describe 'path and ephemeral' do
59
+ it %[should create an ephemeral node with blank data] do
60
+ @zk.create(@base_path, :ephemeral => true)
61
+ @zk.get(@base_path).last.should be_ephemeral
62
+ end
63
+
64
+ it %[should create an ephemeral node with given data] do
65
+ @zk.create(@base_path, 'thedata', :ephemeral => true)
66
+ data, stat = @zk.get(@base_path)
67
+ data.should == 'thedata'
68
+ stat.should be_ephemeral
69
+ end
70
+ end
71
+
72
+ describe 'path and sequential and ephemeral' do
73
+ it %[should create a sequential ephemeral node with blank data] do
74
+ @zk.create(@base_path)
75
+ path = @zk.create("#{@base_path}/v", :sequential => true, :ephemeral => true)
76
+ path.start_with?(@base_path).should be_true
77
+
78
+ File.basename(path).should match(/v\d+/)
79
+
80
+ data, st = @zk.get(path)
81
+ data.should == ''
82
+ st.should be_ephemeral
83
+ end
84
+
85
+ it %[should create a sequential ephemeral node with given data] do
86
+ @zk.create(@base_path)
87
+ path = @zk.create("#{@base_path}/v", 'thedata', :sequential => true, :ephemeral => true)
88
+ path.start_with?(@base_path).should be_true
89
+
90
+ File.basename(path).should match(/v\d+/)
91
+
92
+ data, st = @zk.get(path)
93
+ data.should == 'thedata'
94
+ st.should be_ephemeral
95
+ end
96
+ end
97
+
98
+ it %[should barf if someone hands 3 params] do
99
+ lambda { @zk.create(@base_path, 'data', :sequence) }.should raise_error(ArgumentError)
100
+ end
101
+
102
+ it %[should barf if both :sequence and :sequential are given] do
103
+ lambda { @zk.create(@base_path, 'data', :sequence => true, :sequential => true) }.should raise_error(ArgumentError)
104
+ end
105
+ end
106
+
17
107
  describe :stat do
18
108
  describe 'for a missing node' do
19
109
  before do
@@ -60,7 +150,7 @@ shared_examples_for 'client' do
60
150
 
61
151
  describe 'node exists initially' do
62
152
  before do
63
- @zk.create(@path, '', :mode => :ephemeral)
153
+ @zk.create(@path, :mode => :ephemeral)
64
154
  @zk.exists?(@path).should be_true
65
155
  end
66
156
 
@@ -113,7 +203,7 @@ shared_examples_for 'client' do
113
203
  end
114
204
 
115
205
  @zk.exists?(@path, :watch => true).should be_false
116
- @zk.create(@path, '')
206
+ @zk.create(@path)
117
207
 
118
208
  logger.debug { "waiting for event delivery" }
119
209
 
@@ -151,6 +241,50 @@ shared_examples_for 'client' do
151
241
  ensure_event_delivery!
152
242
  end
153
243
  end
244
+
245
+ end # reopen
246
+
247
+ describe 'reconnection' do
248
+ it %[should if it receives a client_invalid? event] do
249
+ flexmock(@zk) do |m|
250
+ m.should_receive(:reopen).with(0).once
251
+ end
252
+
253
+ bogus_event = flexmock(:expired_session_event, :session_event? => true, :client_invalid? => true, :state_name => 'ZOO_EXPIRED_SESSION_STATE')
254
+
255
+ @zk.raw_event_handler(bogus_event)
256
+ end
257
+ end # reconnection
258
+
259
+ describe :on_exception do
260
+ it %[should register a callback that will be called if an exception is raised on the threadpool] do
261
+ @ary = []
262
+
263
+ @zk.on_exception { |exc| @ary << exc }
264
+
265
+ @zk.defer { raise "ZOMG!" }
266
+
267
+ wait_while(2) { @ary.empty? }
268
+
269
+ @ary.length.should == 1
270
+
271
+ e = @ary.shift
272
+
273
+ e.should be_kind_of(RuntimeError)
274
+ e.message.should == 'ZOMG!'
275
+ end
276
+ end
277
+
278
+ describe :on_threadpool? do
279
+ it %[should be true if we're on the threadpool] do
280
+ @ary = []
281
+
282
+ @zk.defer { @ary << @zk.on_threadpool? }
283
+
284
+ wait_while(2) { @ary.empty? }
285
+ @ary.length.should == 1
286
+ @ary.first.should be_true
287
+ end
154
288
  end
155
289
  end
156
290
 
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
 
4
- $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ # $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
5
+
6
+ Bundler.require(:development, :test)
5
7
 
6
8
  require 'zk'
7
9
  require 'benchmark'
8
10
 
9
- ZK_TEST_PORT = 2181
11
+ ZK_TEST_PORT = 2181 unless defined?(ZK_TEST_PORT)
10
12
 
11
13
  # Requires supporting ruby files with custom matchers and macros, etc,
12
14
  # in spec/support/ and its subdirectories.
@@ -0,0 +1,11 @@
1
+ class EventCatcher < Struct.new(:created, :changed, :deleted, :child, :all)
2
+
3
+ def initialize(*args)
4
+ super
5
+
6
+ [:created, :changed, :deleted, :child, :all].each do |k|
7
+ self.__send__(:"#{k}=", []) if self.__send__(:"#{k}").nil?
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,6 @@
1
+ RSpec::Matchers.define :exist do
2
+ match do |actual|
3
+ actual.exists?
4
+ end
5
+ end
6
+
@@ -1,5 +1,6 @@
1
1
  module ZK
2
- LOG_FILE = File.open(File.join(ZK::ZK_ROOT, 'test.log'), 'a').tap { |f| f.sync = true }
2
+ # LOG_FILE = File.open(File.join(ZK::ZK_ROOT, 'test.log'), 'a').tap { |f| f.sync = true }
3
+ LOG_FILE = File.join(ZK::ZK_ROOT, 'test.log')
3
4
  # LOG_FILE = $stderr
4
5
  end
5
6