zk 0.9.1 → 1.0.0.rc.1

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