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/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
+