zk 0.6.4
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.
- data/.gitignore +7 -0
- data/Gemfile +10 -0
- data/Rakefile +13 -0
- data/lib/z_k/client.rb +906 -0
- data/lib/z_k/election.rb +411 -0
- data/lib/z_k/event_handler.rb +202 -0
- data/lib/z_k/event_handler_subscription.rb +29 -0
- data/lib/z_k/exceptions.rb +101 -0
- data/lib/z_k/extensions.rb +144 -0
- data/lib/z_k/locker.rb +254 -0
- data/lib/z_k/logging.rb +15 -0
- data/lib/z_k/message_queue.rb +143 -0
- data/lib/z_k/mongoid.rb +172 -0
- data/lib/z_k/pool.rb +254 -0
- data/lib/z_k/threadpool.rb +109 -0
- data/lib/z_k/version.rb +3 -0
- data/lib/z_k.rb +73 -0
- data/lib/zk.rb +2 -0
- data/spec/client_pool_spec.rb +329 -0
- data/spec/client_spec.rb +102 -0
- data/spec/election_spec.rb +301 -0
- data/spec/locker_spec.rb +386 -0
- data/spec/log4j.properties +17 -0
- data/spec/message_queue_spec.rb +55 -0
- data/spec/mongoid_spec.rb +330 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/support/bogus_mongoid.rb +11 -0
- data/spec/support/queuey_thread.rb +11 -0
- data/spec/test_file.txt +4 -0
- data/spec/threadpool_spec.rb +71 -0
- data/spec/watch_spec.rb +118 -0
- data/spec/zookeeper_spec.rb +176 -0
- data/zk.gemspec +24 -0
- metadata +176 -0
data/lib/z_k/client.rb
ADDED
@@ -0,0 +1,906 @@
|
|
1
|
+
module ZK
|
2
|
+
# A ruby-friendly wrapper around the low-level zookeeper drivers. This is the
|
3
|
+
# class that you will likely interact with the most.
|
4
|
+
#
|
5
|
+
class Client
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
DEFAULT_TIMEOUT = 10
|
9
|
+
|
10
|
+
attr_reader :event_handler
|
11
|
+
|
12
|
+
attr_reader :cnx #:nodoc:
|
13
|
+
|
14
|
+
# for backwards compatibility
|
15
|
+
alias :watcher :event_handler #:nodoc:
|
16
|
+
|
17
|
+
#:stopdoc:
|
18
|
+
STATE_SYM_MAP = {
|
19
|
+
Zookeeper::ZOO_CLOSED_STATE => :closed,
|
20
|
+
Zookeeper::ZOO_EXPIRED_SESSION_STATE => :expired_session,
|
21
|
+
Zookeeper::ZOO_AUTH_FAILED_STATE => :auth_failed,
|
22
|
+
Zookeeper::ZOO_CONNECTING_STATE => :connecting,
|
23
|
+
Zookeeper::ZOO_CONNECTED_STATE => :connected,
|
24
|
+
Zookeeper::ZOO_ASSOCIATING_STATE => :associating,
|
25
|
+
}.freeze
|
26
|
+
#:startdoc:
|
27
|
+
|
28
|
+
# Create a new client and connect to the zookeeper server.
|
29
|
+
#
|
30
|
+
# +host+ should be a string of comma-separated host:port pairs. You can
|
31
|
+
# also supply an optional "chroot" suffix that will act as an implicit
|
32
|
+
# prefix to all paths supplied.
|
33
|
+
#
|
34
|
+
# example:
|
35
|
+
#
|
36
|
+
# ZK::Client.new("zk01:2181,zk02:2181/chroot/path")
|
37
|
+
#
|
38
|
+
def initialize(host, opts={})
|
39
|
+
@event_handler = EventHandler.new(self)
|
40
|
+
yield self if block_given?
|
41
|
+
@cnx = ::Zookeeper.new(host, DEFAULT_TIMEOUT, @event_handler.get_default_watcher_block)
|
42
|
+
@threadpool = Threadpool.new
|
43
|
+
end
|
44
|
+
|
45
|
+
# Queue an operation to be run on an internal threadpool. You may either
|
46
|
+
# provide an object that responds_to?(:call) or pass a block. There is no
|
47
|
+
# mechanism for retrieving the result of the operation, it is purely
|
48
|
+
# fire-and-forget, so the user is expected to make arrangements for this in
|
49
|
+
# their code.
|
50
|
+
#
|
51
|
+
# An ArgumentError will be raised if +callable+ does not <tt>respond_to?(:call)</tt>
|
52
|
+
#
|
53
|
+
# ==== Arguments
|
54
|
+
# * <tt>callable</tt>: an object that <tt>respond_to?(:call)</tt>, takes precedence
|
55
|
+
# over a given block
|
56
|
+
#
|
57
|
+
def defer(callable=nil, &block)
|
58
|
+
@threadpool.defer(callable, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# returns true if the connection has been closed
|
62
|
+
#--
|
63
|
+
# XXX: should this be *our* idea of closed or ZOO_CLOSED_STATE ?
|
64
|
+
def closed?
|
65
|
+
defined?(::JRUBY_VERSION) ? jruby_closed? : mri_closed?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def jruby_closed?
|
70
|
+
@cnx.state == Java::OrgApacheZookeeper::ZooKeeper::States::CLOSED
|
71
|
+
end
|
72
|
+
|
73
|
+
def mri_closed?
|
74
|
+
@cnx.state or false
|
75
|
+
rescue RuntimeError => e
|
76
|
+
# gah, lame error parsing here
|
77
|
+
raise e if (e.message != 'zookeeper handle is closed') and not defined?(::JRUBY_VERSION)
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
public
|
82
|
+
|
83
|
+
# returns the current state of the connection as reported by the underlying driver
|
84
|
+
# as a symbol. The possible values are <tt>[:closed, :expired_session, :auth_failed
|
85
|
+
# :connecting, :connected, :associating]</tt>.
|
86
|
+
#
|
87
|
+
# See the Zookeeper session
|
88
|
+
# {documentation}[http://hadoop.apache.org/zookeeper/docs/current/zookeeperProgrammers.html#ch_zkSessions]
|
89
|
+
# for more information
|
90
|
+
#
|
91
|
+
def state
|
92
|
+
if defined?(::JRUBY_VERSION)
|
93
|
+
@cnx.state.to_string.downcase.to_sym
|
94
|
+
else
|
95
|
+
STATE_SYM_MAP.fetch(@cnx.state) { |k| raise IndexError, "unrecognized state: #{k}" }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# reopen the underlying connection
|
100
|
+
# returns state of connection after operation
|
101
|
+
def reopen(timeout=10)
|
102
|
+
@cnx.reopen(timeout, @event_handler.get_default_watcher_block)
|
103
|
+
@threadpool.start! # restart the threadpool if previously stopped by close!
|
104
|
+
state
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns true if the underlying connection is in the +connected+ state.
|
108
|
+
def connected?
|
109
|
+
wrap_state_closed_error { @cnx and @cnx.connected? }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns true if the underlying connection is in the +associating+ state.
|
113
|
+
def associating?
|
114
|
+
wrap_state_closed_error { @cnx and @cnx.associating? }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns true if the underlying connection is in the +connecting+ state.
|
118
|
+
def connecting?
|
119
|
+
wrap_state_closed_error { @cnx and @cnx.connecting? }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns true if the underlying connection is in the +expired_session+ state.
|
123
|
+
def expired_session?
|
124
|
+
return nil unless @cnx
|
125
|
+
|
126
|
+
if defined?(::JRUBY_VERSION)
|
127
|
+
@cnx.state == Java::OrgApacheZookeeper::ZooKeeper::States::EXPIRED_SESSION
|
128
|
+
else
|
129
|
+
wrap_state_closed_error { @cnx.state == Zookeeper::ZOO_EXPIRED_SESSION_STATE }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# does a stat on '/', returns true or false
|
134
|
+
def ping? #:nodoc:
|
135
|
+
false unless connected?
|
136
|
+
false|stat('/')
|
137
|
+
rescue ZK::Exceptions::KeeperException
|
138
|
+
false
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create a node with the given path. The node data will be the given data,
|
142
|
+
# and node acl will be the given acl. The path is returned.
|
143
|
+
#
|
144
|
+
# The ephemeral argument specifies whether the created node will be
|
145
|
+
# ephemeral or not.
|
146
|
+
#
|
147
|
+
# An ephemeral node will be removed by the server automatically when the
|
148
|
+
# session associated with the creation of the node expires.
|
149
|
+
#
|
150
|
+
# The sequence argument can also specify to create a sequential node. The
|
151
|
+
# actual path name of a sequential node will be the given path plus a
|
152
|
+
# suffix "_i" where i is the current sequential number of the node. Once
|
153
|
+
# such a node is created, the sequential number will be incremented by one.
|
154
|
+
#
|
155
|
+
# If a node with the same actual path already exists in the ZooKeeper, a
|
156
|
+
# KeeperException with error code KeeperException::NodeExists will be
|
157
|
+
# thrown. Note that since a different actual path is used for each
|
158
|
+
# invocation of creating sequential node with the same path argument, the
|
159
|
+
# call will never throw a NodeExists KeeperException.
|
160
|
+
#
|
161
|
+
# If the parent node does not exist in the ZooKeeper, a KeeperException
|
162
|
+
# with error code KeeperException::NoNode will be thrown.
|
163
|
+
#
|
164
|
+
# An ephemeral node cannot have children. If the parent node of the given
|
165
|
+
# path is ephemeral, a KeeperException with error code
|
166
|
+
# KeeperException::NoChildrenForEphemerals will be thrown.
|
167
|
+
#
|
168
|
+
# This operation, if successful, will trigger all the watches left on the
|
169
|
+
# node of the given path by exists and get API calls, and the watches left
|
170
|
+
# on the parent node by children API calls.
|
171
|
+
#
|
172
|
+
# If a node is created successfully, the ZooKeeper server will trigger the
|
173
|
+
# watches on the path left by exists calls, and the watches on the parent
|
174
|
+
# of the node by children calls.
|
175
|
+
#
|
176
|
+
# Called with a hash of arguments set. Supports being executed
|
177
|
+
# asynchronousy by passing a callback object.
|
178
|
+
#
|
179
|
+
# ==== Arguments
|
180
|
+
# * <tt>path</tt> -- path of the node
|
181
|
+
# * <tt>data</tt> -- initial data for the node, defaults to an empty string
|
182
|
+
# * <tt>:acl</tt> -- defaults to <tt>ACL::OPEN_ACL_UNSAFE</tt>, otherwise the ACL for the node
|
183
|
+
# * <tt>:ephemeral</tt> -- defaults to false, if set to true the created node will be ephemeral
|
184
|
+
# * <tt>:sequence</tt> -- defaults to false, if set to true the created node will be sequential
|
185
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::StringCallback object or
|
186
|
+
# Proc for an asynchronous call to occur
|
187
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
188
|
+
# * <tt>:mode</tt> -- may be specified instead of :ephemeral and :sequence,
|
189
|
+
# accepted values are <tt>[:ephemeral_sequential, :persistent_sequential,
|
190
|
+
# :persistent, :ephemeral]</tt>
|
191
|
+
#
|
192
|
+
# ==== Examples
|
193
|
+
#
|
194
|
+
# ===== create node, no data, persistent
|
195
|
+
#
|
196
|
+
# zk.create("/path")
|
197
|
+
# # => "/path"
|
198
|
+
#
|
199
|
+
# ===== create node, ACL will default to ACL::OPEN_ACL_UNSAFE
|
200
|
+
#
|
201
|
+
# zk.create("/path", "foo")
|
202
|
+
# # => "/path"
|
203
|
+
#
|
204
|
+
# ===== create ephemeral node
|
205
|
+
# zk.create("/path", :mode => :ephemeral)
|
206
|
+
# # => "/path"
|
207
|
+
#
|
208
|
+
# ===== create sequential node
|
209
|
+
# zk.create("/path", :mode => :persistent_sequence)
|
210
|
+
# # => "/path0"
|
211
|
+
#
|
212
|
+
# ===== create ephemeral and sequential node
|
213
|
+
# zk.create("/path", "foo", :mode => :ephemeral_sequence)
|
214
|
+
# # => "/path0"
|
215
|
+
#
|
216
|
+
# ===== create a child path
|
217
|
+
# zk.create("/path/child", "bar")
|
218
|
+
# # => "/path/child"
|
219
|
+
#
|
220
|
+
# ===== create a sequential child path
|
221
|
+
# zk.create("/path/child", "bar", :mode => :ephemeral_sequence)
|
222
|
+
# # => "/path/child0"
|
223
|
+
#
|
224
|
+
#--
|
225
|
+
# TODO: document asynchronous callback
|
226
|
+
#
|
227
|
+
# ===== create asynchronously with callback object
|
228
|
+
#
|
229
|
+
# class StringCallback
|
230
|
+
# def process_result(return_code, path, context, name)
|
231
|
+
# # do processing here
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# callback = StringCallback.new
|
236
|
+
# context = Object.new
|
237
|
+
#
|
238
|
+
# zk.create("/path", "foo", :callback => callback, :context => context)
|
239
|
+
#
|
240
|
+
# ===== create asynchronously with callback proc
|
241
|
+
#
|
242
|
+
# callback = proc do |return_code, path, context, name|
|
243
|
+
# # do processing here
|
244
|
+
# end
|
245
|
+
#
|
246
|
+
# context = Object.new
|
247
|
+
#
|
248
|
+
# zk.create("/path", "foo", :callback => callback, :context => context)
|
249
|
+
#
|
250
|
+
#++
|
251
|
+
def create(path, data='', opts={})
|
252
|
+
h = { :path => path, :data => data, :ephemeral => false, :sequence => false }.merge(opts)
|
253
|
+
|
254
|
+
if mode = h.delete(:mode)
|
255
|
+
mode = mode.to_sym
|
256
|
+
|
257
|
+
case mode
|
258
|
+
when :ephemeral_sequential
|
259
|
+
h[:ephemeral] = h[:sequence] = true
|
260
|
+
when :persistent_sequential
|
261
|
+
h[:ephemeral] = false
|
262
|
+
h[:sequence] = true
|
263
|
+
when :persistent
|
264
|
+
h[:ephemeral] = false
|
265
|
+
when :ephemeral
|
266
|
+
h[:ephemeral] = true
|
267
|
+
else
|
268
|
+
raise ArgumentError, "Unknown mode: #{mode.inspect}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
rv = check_rc(@cnx.create(h))
|
273
|
+
|
274
|
+
h[:callback] ? rv : rv[:path]
|
275
|
+
end
|
276
|
+
|
277
|
+
# Return the data and stat of the node of the given path.
|
278
|
+
#
|
279
|
+
# If the watch is true and the call is successfull (no exception is
|
280
|
+
# thrown), a watch will be left on the node with the given path. The watch
|
281
|
+
# will be triggered by a successful operation that sets data on the node,
|
282
|
+
# or deletes the node. See +watcher+ for documentation on how to register
|
283
|
+
# blocks to be called when a watch event is fired.
|
284
|
+
#
|
285
|
+
# A KeeperException with error code KeeperException::NoNode will be thrown
|
286
|
+
# if no node with the given path exists.
|
287
|
+
#
|
288
|
+
# Supports being executed asynchronousy by passing a callback object.
|
289
|
+
#
|
290
|
+
# ==== Arguments
|
291
|
+
# * <tt>path</tt> -- path of the node
|
292
|
+
# * <tt>:watch</tt> -- defaults to false, set to true if you need to watch this node
|
293
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::DataCallback object or
|
294
|
+
# Proc for an asynchronous call to occur
|
295
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
296
|
+
#
|
297
|
+
# ==== Examples
|
298
|
+
# ===== get data for path
|
299
|
+
# zk.get("/path")
|
300
|
+
#
|
301
|
+
# ===== get data and set watch on node
|
302
|
+
# zk.get("/path", :watch => true)
|
303
|
+
#
|
304
|
+
#--
|
305
|
+
# ===== get data asynchronously
|
306
|
+
#
|
307
|
+
# class DataCallback
|
308
|
+
# def process_result(return_code, path, context, data, stat)
|
309
|
+
# # do processing here
|
310
|
+
# end
|
311
|
+
# end
|
312
|
+
#
|
313
|
+
# zk.get("/path") do |return_code, path, context, data, stat|
|
314
|
+
# # do processing here
|
315
|
+
# end
|
316
|
+
#
|
317
|
+
# callback = DataCallback.new
|
318
|
+
# context = Object.new
|
319
|
+
# zk.get("/path", :callback => callback, :context => context)
|
320
|
+
#++
|
321
|
+
def get(path, opts={})
|
322
|
+
h = { :path => path }.merge(opts)
|
323
|
+
|
324
|
+
setup_watcher!(:data, h)
|
325
|
+
|
326
|
+
rv = check_rc(@cnx.get(h))
|
327
|
+
|
328
|
+
opts[:callback] ? rv : rv.values_at(:data, :stat)
|
329
|
+
end
|
330
|
+
|
331
|
+
# Set the data for the node of the given path if such a node exists and the
|
332
|
+
# given version matches the version of the node (if the given version is
|
333
|
+
# -1, it matches any node's versions). Return the stat of the node.
|
334
|
+
#
|
335
|
+
# This operation, if successful, will trigger all the watches on the node
|
336
|
+
# of the given path left by get_data calls.
|
337
|
+
#
|
338
|
+
# A KeeperException with error code KeeperException::NoNode will be thrown
|
339
|
+
# if no node with the given path exists. A KeeperException with error code
|
340
|
+
# KeeperException::BadVersion will be thrown if the given version does not
|
341
|
+
# match the node's version.
|
342
|
+
#
|
343
|
+
# Called with a hash of arguments set. Supports being executed
|
344
|
+
# asynchronousy by passing a callback object.
|
345
|
+
#
|
346
|
+
# ==== Arguments
|
347
|
+
# * <tt>:path</tt> -- path of the node
|
348
|
+
# * <tt>:data</tt> -- data to set
|
349
|
+
# * <tt>:version</tt> -- defaults to -1, otherwise set to the expected matching version
|
350
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::StatCallback object or
|
351
|
+
# Proc for an asynchronous call to occur
|
352
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
353
|
+
#
|
354
|
+
# ==== Examples
|
355
|
+
# zk.set("/path", "foo")
|
356
|
+
# zk.set("/path", "foo", :version => 0)
|
357
|
+
#
|
358
|
+
#--
|
359
|
+
# ===== set data asynchronously
|
360
|
+
#
|
361
|
+
# class StatCallback
|
362
|
+
# def process_result(return_code, path, context, stat)
|
363
|
+
# # do processing here
|
364
|
+
# end
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# callback = StatCallback.new
|
368
|
+
# context = Object.new
|
369
|
+
#
|
370
|
+
# zk.set("/path", "foo", :callback => callback, :context => context)
|
371
|
+
#++
|
372
|
+
def set(path, data, opts={})
|
373
|
+
h = { :path => path, :data => data }.merge(opts)
|
374
|
+
|
375
|
+
rv = check_rc(@cnx.set(h))
|
376
|
+
|
377
|
+
opts[:callback] ? nil : rv[:stat]
|
378
|
+
end
|
379
|
+
|
380
|
+
# Return the stat of the node of the given path. Return nil if the node
|
381
|
+
# doesn't exist.
|
382
|
+
#
|
383
|
+
# If the watch is true and the call is successful (no exception is thrown),
|
384
|
+
# a watch will be left on the node with the given path. The watch will be
|
385
|
+
# triggered by a successful operation that creates/delete the node or sets
|
386
|
+
# the data on the node.
|
387
|
+
#
|
388
|
+
# Can be called with just the path, otherwise a hash with the arguments
|
389
|
+
# set. Supports being executed asynchronousy by passing a callback object.
|
390
|
+
#
|
391
|
+
# ==== Arguments
|
392
|
+
# * <tt>path</tt> -- path of the node
|
393
|
+
# * <tt>:watch</tt> -- defaults to false, set to true if you need to watch
|
394
|
+
# this node
|
395
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::StatCallback object or
|
396
|
+
# Proc for an asynchronous call to occur
|
397
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
398
|
+
#
|
399
|
+
# ==== Examples
|
400
|
+
# ===== exists for path
|
401
|
+
# zk.stat("/path")
|
402
|
+
# # => ZK::Stat
|
403
|
+
#
|
404
|
+
# ===== exists for path with watch set
|
405
|
+
# zk.stat("/path", :watch => true)
|
406
|
+
# # => ZK::Stat
|
407
|
+
#
|
408
|
+
# ===== exists for non existent path
|
409
|
+
# zk.stat("/non_existent_path")
|
410
|
+
# # => nil
|
411
|
+
#
|
412
|
+
#--
|
413
|
+
# ===== exist node asynchronously
|
414
|
+
#
|
415
|
+
# class StatCallback
|
416
|
+
# def process_result(return_code, path, context, stat)
|
417
|
+
# # do processing here
|
418
|
+
# end
|
419
|
+
# end
|
420
|
+
#
|
421
|
+
# callback = StatCallback.new
|
422
|
+
# context = Object.new
|
423
|
+
#
|
424
|
+
# zk.exists?("/path", :callback => callback, :context => context)
|
425
|
+
#++
|
426
|
+
def stat(path, opts={})
|
427
|
+
h = { :path => path }.merge(opts)
|
428
|
+
|
429
|
+
setup_watcher!(:data, h)
|
430
|
+
|
431
|
+
rv = @cnx.stat(h)
|
432
|
+
|
433
|
+
return rv if opts[:callback]
|
434
|
+
|
435
|
+
case rv[:rc]
|
436
|
+
when Zookeeper::ZOK, Zookeeper::ZNONODE
|
437
|
+
rv[:stat]
|
438
|
+
else
|
439
|
+
check_rc(rv) # throws the appropriate error
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# sugar around stat
|
444
|
+
#
|
445
|
+
# ===== instead of
|
446
|
+
# zk.stat('/path').exists?
|
447
|
+
# # => true
|
448
|
+
#
|
449
|
+
# ===== you can do
|
450
|
+
# zk.exists?('/path')
|
451
|
+
# # => true
|
452
|
+
#
|
453
|
+
# this only works for the synchronous version of stat. for async version,
|
454
|
+
# this method will act *exactly* like stat
|
455
|
+
#
|
456
|
+
def exists?(path, opts={})
|
457
|
+
rv = stat(path, opts)
|
458
|
+
opts[:callback] ? rv : rv.exists?
|
459
|
+
end
|
460
|
+
|
461
|
+
# closes the underlying connection and deregisters all callbacks
|
462
|
+
def close!
|
463
|
+
@event_handler.clear!
|
464
|
+
wrap_state_closed_error { @cnx.close }
|
465
|
+
@threadpool.shutdown
|
466
|
+
nil
|
467
|
+
end
|
468
|
+
|
469
|
+
# Delete the node with the given path. The call will succeed if such a node
|
470
|
+
# exists, and the given version matches the node's version (if the given
|
471
|
+
# version is -1, it matches any node's versions).
|
472
|
+
#
|
473
|
+
# A KeeperException with error code KeeperException::NoNode will be thrown
|
474
|
+
# if the nodes does not exist.
|
475
|
+
#
|
476
|
+
# A KeeperException with error code KeeperException::BadVersion will be
|
477
|
+
# thrown if the given version does not match the node's version.
|
478
|
+
#
|
479
|
+
# A KeeperException with error code KeeperException::NotEmpty will be
|
480
|
+
# thrown if the node has children.
|
481
|
+
#
|
482
|
+
# This operation, if successful, will trigger all the watches on the node
|
483
|
+
# of the given path left by exists API calls, and the watches on the parent
|
484
|
+
# node left by children API calls.
|
485
|
+
#
|
486
|
+
# Can be called with just the path, otherwise a hash with the arguments
|
487
|
+
# set. Supports being executed asynchronousy by passing a callback object.
|
488
|
+
#
|
489
|
+
# ==== Arguments
|
490
|
+
# * <tt>path</tt> -- path of the node to be deleted
|
491
|
+
# * <tt>:version</tt> -- defaults to -1 (deletes any version), otherwise
|
492
|
+
# set to the expected matching version
|
493
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::VoidCallback object or
|
494
|
+
# Proc for an asynchronous call to occur
|
495
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
496
|
+
#
|
497
|
+
# ==== Examples
|
498
|
+
# zk.delete("/path")
|
499
|
+
# zk.delete("/path", :version => 0)
|
500
|
+
#
|
501
|
+
#--
|
502
|
+
# ===== delete node asynchronously
|
503
|
+
#
|
504
|
+
# class VoidCallback
|
505
|
+
# def process_result(return_code, path, context)
|
506
|
+
# # do processing here
|
507
|
+
# end
|
508
|
+
# end
|
509
|
+
#
|
510
|
+
# callback = VoidCallback.new
|
511
|
+
# context = Object.new
|
512
|
+
#
|
513
|
+
# zk.delete(/path", :callback => callback, :context => context)
|
514
|
+
#++
|
515
|
+
def delete(path, opts={})
|
516
|
+
h = { :path => path, :version => -1 }.merge(opts)
|
517
|
+
rv = check_rc(@cnx.delete(h))
|
518
|
+
nil
|
519
|
+
end
|
520
|
+
|
521
|
+
# Return the list of the children of the node of the given path.
|
522
|
+
#
|
523
|
+
# If the watch is true and the call is successful (no exception is thrown),
|
524
|
+
# a watch will be left on the node with the given path. The watch will be
|
525
|
+
# triggered by a successful operation that deletes the node of the given
|
526
|
+
# path or creates/delete a child under the node. See +watcher+ for
|
527
|
+
# documentation on how to register blocks to be called when a watch event
|
528
|
+
# is fired.
|
529
|
+
#
|
530
|
+
# A KeeperException with error code KeeperException::NoNode will be thrown
|
531
|
+
# if no node with the given path exists.
|
532
|
+
#
|
533
|
+
# Can be called with just the path, otherwise a hash with the arguments
|
534
|
+
# set. Supports being executed asynchronousy by passing a callback object.
|
535
|
+
#
|
536
|
+
# ==== Arguments
|
537
|
+
# * <tt>path</tt> -- path of the node
|
538
|
+
# * <tt>:watch</tt> -- defaults to false, set to true if you need to watch
|
539
|
+
# this node
|
540
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::ChildrenCallback object
|
541
|
+
# or Proc for an asynchronous call to occur
|
542
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
543
|
+
#
|
544
|
+
# ==== Examples
|
545
|
+
# ===== get children for path
|
546
|
+
# zk.create("/path", :data => "foo")
|
547
|
+
# zk.create("/path/child", :data => "child1", :sequence => true)
|
548
|
+
# zk.create("/path/child", :data => "child2", :sequence => true)
|
549
|
+
# zk.children("/path")
|
550
|
+
# # => ["child0", "child1"]
|
551
|
+
#
|
552
|
+
# ====== get children and set watch
|
553
|
+
# zk.children("/path", :watch => true)
|
554
|
+
# # => ["child0", "child1"]
|
555
|
+
#
|
556
|
+
#--
|
557
|
+
# ===== get children asynchronously
|
558
|
+
#
|
559
|
+
# class ChildrenCallback
|
560
|
+
# def process_result(return_code, path, context, children)
|
561
|
+
# # do processing here
|
562
|
+
# end
|
563
|
+
# end
|
564
|
+
#
|
565
|
+
# callback = ChildrenCallback.new
|
566
|
+
# context = Object.new
|
567
|
+
# zk.children("/path", :callback => callback, :context => context)
|
568
|
+
#++
|
569
|
+
def children(path, opts={})
|
570
|
+
h = { :path => path }.merge(opts)
|
571
|
+
|
572
|
+
setup_watcher!(:child, h)
|
573
|
+
|
574
|
+
rv = check_rc(@cnx.get_children(h))
|
575
|
+
opts[:callback] ? nil : rv[:children]
|
576
|
+
end
|
577
|
+
|
578
|
+
# Return the ACL and stat of the node of the given path.
|
579
|
+
#
|
580
|
+
# A KeeperException with error code KeeperException::Code::NoNode will be
|
581
|
+
# thrown if no node with the given path exists.
|
582
|
+
#
|
583
|
+
# Can be called with just the path, otherwise a hash with the arguments
|
584
|
+
# set. Supports being executed asynchronousy by passing a callback object.
|
585
|
+
#
|
586
|
+
# ==== Arguments
|
587
|
+
# * <tt>path</tt> -- path of the node
|
588
|
+
# * <tt>:stat</tt> -- defaults to nil, provide a Stat object that will be
|
589
|
+
# set with the Stat information of the node path (TODO: test this)
|
590
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::AclCallback object or
|
591
|
+
# Proc for an asynchronous call to occur
|
592
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
593
|
+
#
|
594
|
+
# ==== Examples
|
595
|
+
# ===== get acl
|
596
|
+
# zk.get_acl("/path")
|
597
|
+
# # => [ACL]
|
598
|
+
#
|
599
|
+
# ===== get acl with stat
|
600
|
+
# stat = ZK::Stat.new
|
601
|
+
# zk.get_acl("/path", :stat => stat)
|
602
|
+
#
|
603
|
+
#--
|
604
|
+
# ===== get acl asynchronously
|
605
|
+
#
|
606
|
+
# class AclCallback
|
607
|
+
# def processResult(return_code, path, context, acl, stat)
|
608
|
+
# # do processing here
|
609
|
+
# end
|
610
|
+
# end
|
611
|
+
#
|
612
|
+
# callback = AclCallback.new
|
613
|
+
# context = Object.new
|
614
|
+
# zk.acls("/path", :callback => callback, :context => context)
|
615
|
+
#++
|
616
|
+
def get_acl(path, opts={})
|
617
|
+
h = { :path => path }.merge(opts)
|
618
|
+
rv = check_rc(@cnx.get_acl(h))
|
619
|
+
opts[:callback] ? nil : rv.values_at(:children, :stat)
|
620
|
+
end
|
621
|
+
|
622
|
+
# Set the ACL for the node of the given path if such a node exists and the
|
623
|
+
# given version matches the version of the node. Return the stat of the
|
624
|
+
# node.
|
625
|
+
#
|
626
|
+
# A KeeperException with error code KeeperException::Code::NoNode will be
|
627
|
+
# thrown if no node with the given path exists.
|
628
|
+
#
|
629
|
+
# A KeeperException with error code KeeperException::Code::BadVersion will
|
630
|
+
# be thrown if the given version does not match the node's version.
|
631
|
+
#
|
632
|
+
# Called with a hash of arguments set. Supports being executed
|
633
|
+
# asynchronousy by passing a callback object.
|
634
|
+
#
|
635
|
+
# ==== Arguments
|
636
|
+
# * <tt>path</tt> -- path of the node
|
637
|
+
# * <tt>:acl</tt> -- acl to set
|
638
|
+
# * <tt>:version</tt> -- defaults to -1, otherwise set to the expected matching version
|
639
|
+
# * <tt>:callback</tt> -- provide a AsyncCallback::StatCallback object or
|
640
|
+
# Proc for an asynchronous call to occur
|
641
|
+
# * <tt>:context</tt> -- context object passed into callback method
|
642
|
+
#
|
643
|
+
# ==== Examples
|
644
|
+
# TBA - waiting on clarification of method use
|
645
|
+
#
|
646
|
+
def set_acl(path, acls, opts={})
|
647
|
+
h = { :path => path, :acl => acls }.merge(opts)
|
648
|
+
rv = check_rc(@cnx.set_acl(h))
|
649
|
+
opts[:callback] ? nil : rv[:stat]
|
650
|
+
end
|
651
|
+
|
652
|
+
|
653
|
+
|
654
|
+
#--
|
655
|
+
#
|
656
|
+
# EXTENSIONS
|
657
|
+
#
|
658
|
+
# convenience methods for dealing with zookeeper (rm -rf, mkdir -p, etc)
|
659
|
+
#
|
660
|
+
#++
|
661
|
+
|
662
|
+
# Creates all parent paths and 'path' in zookeeper as persistent nodes with
|
663
|
+
# zero data.
|
664
|
+
#
|
665
|
+
# ==== Arguments
|
666
|
+
# * <tt>path</tt>: An absolute znode path to create
|
667
|
+
#
|
668
|
+
# ==== Examples
|
669
|
+
#
|
670
|
+
# zk.exists?('/path')
|
671
|
+
# # => false
|
672
|
+
#
|
673
|
+
# zk.mkdir_p('/path/to/blah')
|
674
|
+
# # => "/path/to/blah"
|
675
|
+
#
|
676
|
+
#--
|
677
|
+
# TODO: write a non-recursive version of this. ruby doesn't have TCO, so
|
678
|
+
# this could get expensive w/ psychotically long paths
|
679
|
+
def mkdir_p(path)
|
680
|
+
create(path, '', :mode => :persistent)
|
681
|
+
rescue Exceptions::NodeExists
|
682
|
+
return
|
683
|
+
rescue Exceptions::NoNode
|
684
|
+
if File.dirname(path) == '/'
|
685
|
+
# ok, we're screwed, blow up
|
686
|
+
raise KeeperException, "could not create '/', something is wrong", caller
|
687
|
+
end
|
688
|
+
|
689
|
+
mkdir_p(File.dirname(path))
|
690
|
+
retry
|
691
|
+
end
|
692
|
+
|
693
|
+
# recursively remove all children of path then remove path itself
|
694
|
+
def rm_rf(paths)
|
695
|
+
Array(paths).flatten.each do |path|
|
696
|
+
begin
|
697
|
+
children(path).each do |child|
|
698
|
+
rm_rf(File.join(path, child))
|
699
|
+
end
|
700
|
+
|
701
|
+
delete(path)
|
702
|
+
nil
|
703
|
+
rescue Exceptions::NoNode
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
# will block the caller until +abs_node_path+ has been removed
|
709
|
+
#
|
710
|
+
# NOTE: this is dangerous to use in callbacks! there is only one
|
711
|
+
# event-delivery thread, so if you use this method in a callback or
|
712
|
+
# watcher, you *will* deadlock!
|
713
|
+
def block_until_node_deleted(abs_node_path)
|
714
|
+
queue = Queue.new
|
715
|
+
ev_sub = nil
|
716
|
+
|
717
|
+
node_deletion_cb = lambda do |event|
|
718
|
+
if event.node_deleted?
|
719
|
+
queue.enq(:deleted)
|
720
|
+
else
|
721
|
+
queue.enq(:deleted) unless exists?(abs_node_path, :watch => true)
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
ev_sub = watcher.register(abs_node_path, &node_deletion_cb)
|
726
|
+
|
727
|
+
# set up the callback, but bail if we don't need to wait
|
728
|
+
return true unless exists?(abs_node_path, :watch => true)
|
729
|
+
|
730
|
+
queue.pop # block waiting for node deletion
|
731
|
+
true
|
732
|
+
ensure
|
733
|
+
# be sure we clean up after ourselves
|
734
|
+
ev_sub.unregister if ev_sub
|
735
|
+
end
|
736
|
+
|
737
|
+
# creates a new locker based on the name you send in
|
738
|
+
#
|
739
|
+
# see ZK::Locker::ExclusiveLocker
|
740
|
+
#
|
741
|
+
# returns a ZK::Locker::ExclusiveLocker instance using this Client and provided
|
742
|
+
# lock name
|
743
|
+
#
|
744
|
+
# ==== Arguments
|
745
|
+
# * <tt>name</tt> name of the lock you wish to use
|
746
|
+
#
|
747
|
+
# ==== Examples
|
748
|
+
#
|
749
|
+
# zk.locker("blah")
|
750
|
+
# # => #<ZK::Locker::ExclusiveLocker:0x102034cf8 ...>
|
751
|
+
#
|
752
|
+
def locker(name)
|
753
|
+
Locker.exclusive_locker(self, name)
|
754
|
+
end
|
755
|
+
|
756
|
+
# create a new shared locking instance based on the name given
|
757
|
+
#
|
758
|
+
# returns a ZK::Locker::SharedLocker instance using this Client and provided
|
759
|
+
# lock name
|
760
|
+
#
|
761
|
+
# ==== Arguments
|
762
|
+
# * <tt>name</tt> name of the lock you wish to use
|
763
|
+
#
|
764
|
+
# ==== Examples
|
765
|
+
#
|
766
|
+
# zk.shared_locker("blah")
|
767
|
+
# # => #<ZK::Locker::SharedLocker:0x102034cf8 ...>
|
768
|
+
#
|
769
|
+
def shared_locker(name)
|
770
|
+
Locker.shared_locker(self, name)
|
771
|
+
end
|
772
|
+
|
773
|
+
# Convenience method for acquiring a lock then executing a code block. This
|
774
|
+
# will block the caller until the lock is acquired.
|
775
|
+
#
|
776
|
+
# ==== Arguments
|
777
|
+
# * <tt>name</tt>: the name of the lock to use
|
778
|
+
# * <tt>:mode</tt>: either :shared or :exclusive, defaults to :exclusive
|
779
|
+
#
|
780
|
+
# ==== Examples
|
781
|
+
#
|
782
|
+
# zk.with_lock('foo') do
|
783
|
+
# # this code is executed while holding the lock
|
784
|
+
# end
|
785
|
+
#
|
786
|
+
def with_lock(name, opts={}, &b)
|
787
|
+
mode = opts[:mode] || :exclusive
|
788
|
+
|
789
|
+
raise ArgumentError, ":mode option must be either :shared or :exclusive, not #{mode.inspect}" unless [:shared, :exclusive].include?(mode)
|
790
|
+
|
791
|
+
if mode == :shared
|
792
|
+
shared_locker(name).with_lock(&b)
|
793
|
+
else
|
794
|
+
locker(name).with_lock(&b)
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
# Convenience method for constructing a ZK::Election::Candidate object using this
|
799
|
+
# Client connection, the given election +name+ and +data+.
|
800
|
+
#
|
801
|
+
def election_candidate(name, data, opts={})
|
802
|
+
opts = opts.merge(:data => data)
|
803
|
+
ZK::Election::Candidate.new(self, name, opts)
|
804
|
+
end
|
805
|
+
|
806
|
+
# Convenience method for constructing a ZK::Election::Observer object using this
|
807
|
+
# Client connection, and the given election +name+.
|
808
|
+
#
|
809
|
+
def election_observer(name, opts={})
|
810
|
+
ZK::Election::Observer.new(self, name, opts)
|
811
|
+
end
|
812
|
+
|
813
|
+
# creates a new message queue of name +name+
|
814
|
+
#
|
815
|
+
# returns a ZK::MessageQueue object
|
816
|
+
#
|
817
|
+
# ==== Arguments
|
818
|
+
# * <tt>name</tt> the name of the queue
|
819
|
+
#
|
820
|
+
# ==== Examples
|
821
|
+
#
|
822
|
+
# zk.queue("blah").publish({:some_data => "that is yaml serializable"})
|
823
|
+
#
|
824
|
+
def queue(name)
|
825
|
+
MessageQueue.new(self, name)
|
826
|
+
end
|
827
|
+
|
828
|
+
def set_debug_level(level) #:nodoc:
|
829
|
+
if defined?(::JRUBY_VERSION)
|
830
|
+
warn "set_debug_level is not implemented for JRuby"
|
831
|
+
return
|
832
|
+
else
|
833
|
+
num =
|
834
|
+
case level
|
835
|
+
when String, Symbol
|
836
|
+
ZookeeperBase.const_get(:"ZOO_LOG_LEVEL_#{level.to_s.upcase}") rescue NameError
|
837
|
+
when Integer
|
838
|
+
level
|
839
|
+
end
|
840
|
+
|
841
|
+
raise ArgumentError, "#{level.inspect} is not a valid argument to set_debug_level" unless num
|
842
|
+
|
843
|
+
@cnx.set_debug_level(num)
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
# Register a block to be called on connection, when the client has
|
848
|
+
# connected. The block will *always* be called asynchronously (on a
|
849
|
+
# background thread).
|
850
|
+
#
|
851
|
+
# the block will be called with no arguments
|
852
|
+
#
|
853
|
+
# returns an EventHandlerSubscription object that can be used to unregister
|
854
|
+
# this block from further updates
|
855
|
+
#
|
856
|
+
def on_connected(&block)
|
857
|
+
watcher.register_state_handler(:connected, &block).tap do
|
858
|
+
defer { block.call } if connected?
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
# register a block to be called when the client is attempting to reconnect
|
863
|
+
# to the zookeeper server. the documentation says that this state should be
|
864
|
+
# taken to mean that the application should enter into "safe mode" and operate
|
865
|
+
# conservatively, as it won't be getting updates until it has reconnected
|
866
|
+
#
|
867
|
+
def on_connecting(&block)
|
868
|
+
watcher.register_state_handler(:connecting, &block).tap do
|
869
|
+
defer { block.call } if connecting?
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
# register a block to be called when our session has expired. This usually happens
|
874
|
+
# due to a network partitioning event, and means that all callbacks and watches must
|
875
|
+
# be re-registered with the server
|
876
|
+
#---
|
877
|
+
# NOTE: need to come up with a way to test this
|
878
|
+
def on_expired_session(&block)
|
879
|
+
watcher.register_state_handler(:expired_session, &block).tap do
|
880
|
+
defer { block.call } if expired_session?
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
protected
|
885
|
+
def wrap_state_closed_error
|
886
|
+
yield
|
887
|
+
rescue RuntimeError => e
|
888
|
+
# gah, lame error parsing here
|
889
|
+
raise e unless e.message == 'zookeeper handle is closed'
|
890
|
+
false
|
891
|
+
end
|
892
|
+
|
893
|
+
def check_rc(hash)
|
894
|
+
hash.tap do |h|
|
895
|
+
if code = h[:rc]
|
896
|
+
raise Exceptions::KeeperException.by_code(code) unless code == Zookeeper::ZOK
|
897
|
+
end
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
def setup_watcher!(watch_type, opts)
|
902
|
+
@event_handler.setup_watcher!(watch_type, opts)
|
903
|
+
end
|
904
|
+
end
|
905
|
+
end
|
906
|
+
|