├── .dev_extras ├── rspec ├── rvmrc ├── README └── slyphon-project.vimrc ├── lib ├── zk-eventmachine.rb └── z_k │ ├── z_k_event_machine │ ├── version.rb │ ├── event_handler_e_m.rb │ ├── unixisms.rb │ ├── iterator.rb │ ├── callback.rb │ └── client.rb │ └── z_k_event_machine.rb ├── .gitmodules ├── .gitignore ├── .yardopts ├── Guardfile ├── Gemfile ├── spec ├── spec_helper.rb ├── support │ ├── logging_progress_bar_formatter.rb │ ├── wait_watchers.rb │ ├── extensions.rb │ └── logging.rb └── z_k │ └── z_k_event_machine │ ├── event_handler_e_m_spec.rb │ ├── unixisms_spec.rb │ ├── callback_spec.rb │ └── client_spec.rb ├── zk-eventmachine.gemspec ├── LICENSE ├── Rakefile └── README.markdown /.dev_extras/rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.dev_extras/rvmrc: -------------------------------------------------------------------------------- 1 | rvm 1.9.3@zk-em 2 | -------------------------------------------------------------------------------- /lib/zk-eventmachine.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../z_k/z_k_event_machine', __FILE__) 2 | 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "releaseops"] 2 | path = releaseops 3 | url = git://github.com/slyphon/releaseops.git 4 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/version.rb: -------------------------------------------------------------------------------- 1 | module ZK 2 | module ZKEventMachine 3 | VERSION = "1.0.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.* 4 | pkg/* 5 | .rvmrc 6 | .vimrc 7 | .rspec 8 | *.log* 9 | .yardoc 10 | doc 11 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --no-private 3 | --tag hidden_example:Hidden 4 | --hide-tag hidden_example 5 | - 6 | LICENSE 7 | README.markdown 8 | 9 | -------------------------------------------------------------------------------- /.dev_extras/README: -------------------------------------------------------------------------------- 1 | A small place to hide goodies that I use while developing this 2 | 3 | slyphon-project.vimrc is a local .vimrc file with abbrevs. 4 | rvmrc is my .rvmrc 5 | 6 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'bundler' do 2 | watch 'Gemfile' 3 | watch /\A.+\.gemspec\Z/ 4 | end 5 | 6 | guard 'rspec', :version => 2 do 7 | watch(%r{^spec/.+_spec\.rb$}) 8 | 9 | watch(%r{^lib/(.+)\.rb$}) do |m| 10 | generic = "spec/#{m[1]}_spec.rb" 11 | 12 | if test(?f, generic) 13 | generic 14 | else 15 | 'spec' 16 | end 17 | end 18 | 19 | watch(%r%^spec/(spec_helper.rb|support|shared)(?:$|/)%) { "spec" } 20 | 21 | end 22 | 23 | -------------------------------------------------------------------------------- /.dev_extras/slyphon-project.vimrc: -------------------------------------------------------------------------------- 1 | iabbr _ZEM ZK::ZKEventMachine 2 | iabbr _ZCB ZK::ZKEventMachine::Callback 3 | iabbr _ZCLI ZK::ZKEventMachine::Client 4 | 5 | iabbr _wia i.with_indifferent_access 6 | iabbr _lam lambda {}hi 7 | 8 | iabbr _seq i.should == 9 | iabbr _srex i.should raise_exception()i 10 | iabbr _srexarg i.should raise_exception(ArgumentError)i 11 | iabbr _snrex i.should_not raise_exception 12 | iabbr _sbnil i.should be_nil 13 | 14 | iabbr _desc describe do endkea 15 | iabbr _bef before do endkA 16 | 17 | 18 | " vim:ft=vim 19 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | 3 | require 'zookeeper' 4 | require 'zookeeper/em_client' 5 | 6 | require 'zk' 7 | require 'deferred' 8 | 9 | module ZK 10 | module ZKEventMachine 11 | end 12 | end 13 | 14 | 15 | $LOAD_PATH.unshift(File.expand_path('../..', __FILE__)).uniq! 16 | 17 | require 'z_k/z_k_event_machine/iterator' 18 | require 'z_k/z_k_event_machine/callback' 19 | require 'z_k/z_k_event_machine/event_handler_e_m' 20 | require 'z_k/z_k_event_machine/unixisms' 21 | require 'z_k/z_k_event_machine/client' 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/event_handler_e_m.rb: -------------------------------------------------------------------------------- 1 | module ZK 2 | module ZKEventMachine 3 | # a small wrapper around the EventHandler instance, allowing us to 4 | # deliver the event on the reactor thread, as opposed to calling it directly 5 | # 6 | class EventHandlerEM < ZK::EventHandler 7 | include ZK::Logging 8 | 9 | def process(event) 10 | EM.schedule { super(event) } 11 | end 12 | 13 | protected 14 | # we're running on the Reactor, don't need to synchronize (hah, hah, we'll see...) 15 | # 16 | def synchronize 17 | yield 18 | end 19 | end 20 | end 21 | end 22 | 23 | 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | # gem 'zk', :path => '~/zk' 4 | 5 | group :test do 6 | gem 'rspec', '~> 2.8.0' 7 | gem 'flexmock', '~> 0.8.10' 8 | gem 'evented-spec','~> 0.9.0' 9 | end 10 | 11 | group :docs do 12 | gem 'yard', '~> 0.8.0' 13 | 14 | platform :mri_19 do 15 | gem 'redcarpet' 16 | end 17 | end 18 | 19 | group :development do 20 | gem 'guard', :require => false 21 | gem 'guard-rspec', :require => false 22 | gem 'guard-shell', :require => false 23 | gem 'guard-bundler', :require => false 24 | 25 | if RUBY_PLATFORM =~ /darwin/i 26 | gem 'growl', :require => false 27 | gem 'rb-readline', :platform => :ruby 28 | end 29 | 30 | gem 'rake' 31 | gem 'pry' 32 | end 33 | 34 | # Specify your gem's dependencies in zk-em.gemspec 35 | gemspec 36 | 37 | # vim:ft=ruby 38 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) 5 | 6 | require 'zk-eventmachine' 7 | require 'evented-spec' 8 | 9 | # Requires supporting ruby files with custom matchers and macros, etc, 10 | # in spec/support/ and its subdirectories. 11 | Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f} 12 | 13 | case `uname -s`.chomp 14 | when 'Linux' 15 | # $stderr.puts "WARN: setting EM.epoll = true for tests" 16 | EM.epoll = true 17 | when 'Darwin' 18 | # $stderr.puts "WARN: setting EM.kqueue = true for tests" 19 | EM.kqueue = true 20 | end 21 | 22 | RSpec.configure do |config| 23 | config.mock_with :flexmock 24 | config.extend SpecGlobalLogger 25 | config.include SpecGlobalLogger 26 | config.extend WaitWatchers 27 | config.include WaitWatchers 28 | end 29 | 30 | Thread.current[:name] = 'main' 31 | 32 | 33 | -------------------------------------------------------------------------------- /spec/support/logging_progress_bar_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/formatters/progress_formatter' 2 | 3 | # module Motionbox 4 | # # essentially a monkey-patch to the ProgressBarFormatter, outputs 5 | # # '== #{example_proxy.description} ==' in the logs before each test. makes it 6 | # # easier to match up tests with the SQL they produce 7 | # class LoggingProgressBarFormatter < RSpec::Core::Formatters::ProgressFormatter 8 | # def example_started(example) 9 | # ZK.logger.info(yellow("\n=====<([ #{example.full_description} ])>=====\n")) 10 | # super 11 | # end 12 | # end 13 | # end 14 | 15 | module RSpec 16 | module Core 17 | module Formatters 18 | class ProgressFormatter 19 | def example_started(example) 20 | ::Logging.logger['spec'].write(yellow("\n=====<([ #{example.full_description} ])>=====\n\n")) 21 | super(example) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /zk-eventmachine.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "z_k/z_k_event_machine/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "zk-eventmachine" 7 | s.version = ZK::ZKEventMachine::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Jonathan D. Simms"] 10 | s.email = ["slyphon@hp.com"] 11 | s.homepage = "https://github.com/slyphon/zk-eventmachine" 12 | s.summary = %q{ZK client for EventMachine-based (async) applications} 13 | s.description = s.description 14 | 15 | s.add_dependency 'zk', '~> 1.6.2' 16 | 17 | s.add_dependency 'eventmachine', '~> 1.0.0.beta.4' 18 | s.add_dependency 'deferred', '~> 0.5.3' 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/wait_watchers.rb: -------------------------------------------------------------------------------- 1 | module WaitWatchers 2 | class TimeoutError < StandardError; end 3 | 4 | # method to wait until block passed returns true or timeout (default is 10 seconds) is reached 5 | # raises TiemoutError on timeout 6 | def wait_until(timeout=2) 7 | time_to_stop = Time.now + timeout 8 | while true 9 | rval = yield 10 | return rval if rval 11 | raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop 12 | Thread.pass 13 | end 14 | end 15 | 16 | # inverse of wait_until 17 | def wait_while(timeout=2) 18 | time_to_stop = Time.now + timeout 19 | while true 20 | rval = yield 21 | return rval unless rval 22 | raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop 23 | Thread.pass 24 | end 25 | end 26 | 27 | def report_realtime(what) 28 | return yield 29 | t = Benchmark.realtime { yield } 30 | $stderr.puts "#{what}: %0.3f" % [t.to_f] 31 | end 32 | end 33 | 34 | 35 | -------------------------------------------------------------------------------- /spec/support/extensions.rb: -------------------------------------------------------------------------------- 1 | # method to wait until block passed returns true or timeout (default is 2 seconds) is reached 2 | def wait_until(timeout=2) 3 | time_to_stop = Time.now + timeout 4 | 5 | until yield 6 | break if Time.now > time_to_stop 7 | Thread.pass 8 | end 9 | end 10 | 11 | def wait_while(timeout=2) 12 | time_to_stop = Time.now + timeout 13 | 14 | while yield 15 | break if Time.now > time_to_stop 16 | Thread.pass 17 | end 18 | end 19 | 20 | class ::Thread 21 | # join with thread until given block is true, the thread joins successfully, 22 | # or timeout seconds have passed 23 | # 24 | def join_until(timeout=2) 25 | time_to_stop = Time.now + timeout 26 | 27 | until yield 28 | break if Time.now > time_to_stop 29 | break if join(0.1) 30 | end 31 | end 32 | 33 | def join_while(timeout=2) 34 | time_to_stop = Time.now + timeout 35 | 36 | while yield 37 | break if Time.now > time_to_stop 38 | break if join(0.1) 39 | end 40 | end 41 | end 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Hewlett Packard Development Company, L.P. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | gemset_name = 'zk-em' 2 | 3 | release_ops_path = File.expand_path('../releaseops/lib', __FILE__) 4 | 5 | # if the special submodule is availabe, use it 6 | # we use a submodule because it doesn't depend on anything else (*cough* bundler) 7 | # and can be shared across projects 8 | # 9 | if File.exists?(release_ops_path) 10 | require File.join(release_ops_path, 'releaseops') 11 | 12 | # sets up the multi-ruby zk:test_all rake tasks 13 | ReleaseOps::TestTasks.define_for(*%w[1.8.7 1.9.2 jruby ree 1.9.3]) 14 | 15 | # sets up the task :default => 'spec:run' and defines a simple 16 | # "run the specs with the current rvm profile" task 17 | ReleaseOps::TestTasks.define_simple_default_for_travis 18 | 19 | # Define a task to run code coverage tests 20 | ReleaseOps::TestTasks.define_simplecov_tasks 21 | 22 | # set up yard:server, yard:gems, and yard:clean tasks 23 | # for doing documentation stuff 24 | ReleaseOps::YardTasks.define 25 | 26 | ReleaseOps::GemTasks.define('zk-eventmachine.gemspec') 27 | end 28 | 29 | task 'mb:test_all' => 'zk:test_all' 30 | 31 | namespace :yard do 32 | task :clean do 33 | rm_rf '.yardoc' 34 | end 35 | 36 | task :server => :clean do 37 | sh "yard server --reload --port=8810" 38 | end 39 | 40 | task :gems do 41 | sh 'yard server --gems --port=8811' 42 | end 43 | end 44 | 45 | task :clean => 'yard:clean' 46 | 47 | 48 | -------------------------------------------------------------------------------- /spec/support/logging.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | 4 | module ZK 5 | TEST_LOG_PATH = File.expand_path('../../../test.log', __FILE__) 6 | 7 | def self.logging_gem_setup 8 | layout_opts = { 9 | :pattern => '%.1l, [%d #%p] (%9.9T) %25.25c{2}: %m\n', 10 | } 11 | 12 | layout_opts[:date_pattern] = ZK.jruby? ? '%H:%M:%S.%3N' : '%H:%M:%S.%6N' 13 | 14 | layout = ::Logging.layouts.pattern(layout_opts) 15 | 16 | appender = ENV['ZK_DEBUG'] ? ::Logging.appenders.stderr : ::Logging.appenders.file(ZK::TEST_LOG_PATH) 17 | appender.layout = layout 18 | # appender.immediate_at = "debug,info,warn,error,fatal" 19 | # appender.auto_flushing = true 20 | appender.auto_flushing = 25 21 | appender.flush_period = 5 22 | 23 | %w[ZK ClientForker spec Zookeeper].each do |name| 24 | ::Logging.logger[name].tap do |log| 25 | log.appenders = [appender] 26 | log.level = :debug 27 | end 28 | end 29 | 30 | # this logger is kinda noisy 31 | ::Logging.logger['ZK::EventHandler'].level = :info 32 | 33 | Zookeeper.logger = ::Logging.logger['Zookeeper'] 34 | Zookeeper.logger.level = ENV['ZOOKEEPER_DEBUG'] ? :debug : :warn 35 | 36 | ZK::ForkHook.after_fork_in_child { ::Logging.reopen } 37 | end 38 | 39 | def self.stdlib_logger_setup 40 | require 'logger' 41 | log = ::Logger.new($stderr).tap {|l| l.level = ::Logger::DEBUG } 42 | ZK.logger = log 43 | Zookeeper.logger = log 44 | end 45 | end 46 | 47 | ZK.logging_gem_setup 48 | 49 | # for debugging along with C output comment above and 50 | # uncomment the following 51 | # 52 | # ZK.stdlib_logger_setup 53 | # $stderr.sync = true 54 | # logger = Logger.new($stderr).tap { |l| l.level = Logger::DEBUG } 55 | # Zookeeper.set_debug_level(4) 56 | 57 | module SpecGlobalLogger 58 | def logger 59 | @spec_global_logger ||= ::Logging.logger['spec'] 60 | end 61 | 62 | # sets the log level to FATAL for the duration of the block 63 | def mute_logger 64 | zk_log = Logging.logger['ZK'] 65 | orig_level, zk_log.level = zk_log.level, :off 66 | orig_zoo_level, Zookeeper.debug_level = Zookeeper.debug_level, Zookeeper::Constants::ZOO_LOG_LEVEL_ERROR 67 | yield 68 | ensure 69 | zk_log.level = orig_level 70 | Zookeeper.debug_level = orig_zoo_level 71 | end 72 | end 73 | 74 | 75 | -------------------------------------------------------------------------------- /spec/z_k/z_k_event_machine/event_handler_e_m_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module ZK::ZKEventMachine 4 | describe 'EventHandlerEM' do 5 | include EventedSpec::SpecHelper 6 | default_timeout 2.0 7 | 8 | before do 9 | @zk = ::ZK.new 10 | @base_path = '/zk-em-testing' 11 | @zk.rm_rf(@base_path) 12 | @zk.mkdir_p(@base_path) 13 | @zkem = ZK::ZKEventMachine::Client.new('localhost:2181') 14 | end 15 | 16 | after do 17 | mute_logger do 18 | @zk.rm_rf(@base_path) 19 | @zk.close! 20 | end 21 | end 22 | 23 | # this is a test of event delivery in general, not just of the 24 | # EventHandlerEM implementation 25 | 26 | describe 'data event' do 27 | include EventedSpec::EMSpec 28 | 29 | before do 30 | @path = "#{@base_path}/blah" 31 | @data = "this is data" 32 | @new_data = "this is other data" 33 | 34 | @child_path = [@path, 'child'].join('/') 35 | end 36 | 37 | it %[should call the callback when the data of a watched node changes] do 38 | @zkem.connect do 39 | @zkem.event_handler.register(@path) do |event| 40 | EM.reactor_thread?.should be_true 41 | event.should be_node_changed 42 | @zkem.close! { done } 43 | end 44 | 45 | common_eb = lambda { |exc| raise exc } 46 | 47 | @zkem.create(@path, @data) do |exc,path| 48 | raise exc if exc 49 | 50 | @zkem.stat(@path, :watch => true) do |e,*a| 51 | raise e if e 52 | 53 | @zkem.set(@path, @new_data) do |e,*a| 54 | raise e if e 55 | end 56 | end 57 | end 58 | end 59 | end 60 | 61 | it %[should call the callback when the children of the watched node change] do 62 | @zkem.connect do 63 | @zkem.event_handler.register(@path) do |event| 64 | EM.reactor_thread?.should be_true 65 | event.should be_node_child 66 | @zkem.close! { done } 67 | end 68 | 69 | eb_raise = lambda { |e| raise e if e } 70 | 71 | @zkem.create(@path, @data).callback { |*| 72 | @zkem.children(@path, :watch => true).callback { |ary,stat| 73 | logger.debug { "called back with: #{ary.inspect}" } 74 | ary.should be_empty 75 | stat.should be_kind_of(Zookeeper::Stat) 76 | 77 | @zkem.create(@child_path, '').callback { |p| 78 | p.should == @child_path 79 | 80 | }.errback(&eb_raise) 81 | }.errback(&eb_raise) 82 | }.errback(&eb_raise) 83 | end 84 | end # it 85 | 86 | it %[should call back the registered block when the node is deleted] do 87 | @zkem.connect do 88 | @zkem.event_handler.register(@path) do |event| 89 | EM.reactor_thread?.should be_true 90 | event.should be_node_deleted 91 | @zkem.close! { done } 92 | end 93 | 94 | eb_raise = lambda { |e| raise e if e } 95 | 96 | @zkem.create(@path, @data).callback do |*| 97 | @zkem.stat(@path, :watch => true).callback do |*| 98 | @zkem.delete(@path).errback(&eb_raise) 99 | 100 | end.errback(&eb_raise) 101 | end.errback(&eb_raise) 102 | end 103 | end 104 | end 105 | end 106 | end 107 | 108 | -------------------------------------------------------------------------------- /spec/z_k/z_k_event_machine/unixisms_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module ZK::ZKEventMachine 4 | describe 'Unixisms' do 5 | include EventedSpec::SpecHelper 6 | default_timeout 2.0 7 | 8 | before do 9 | @zk = ::ZK.new 10 | @base_path = '/zk-em-testing' 11 | @zk.rm_rf(@base_path) 12 | @zk.mkdir_p(@base_path) 13 | @zkem = ZK::ZKEventMachine::Client.new('localhost:2181') 14 | end 15 | 16 | after do 17 | @zk.rm_rf(@base_path) 18 | @zk.close! 19 | end 20 | 21 | def close_and_done! 22 | @zkem.close! { done } 23 | end 24 | 25 | describe 'mkdir_p' do 26 | before do 27 | @bogus_paths = [ 28 | [@base_path, 'bogus', 'path', 'to', 'qwer'].join('/'), 29 | [@base_path, 'bogus', 'path', 'to', 'somethingelse'].join('/') 30 | ] 31 | end 32 | 33 | it %[should create the path recursively] do 34 | @zk.exists?(@bogus_paths.first).should be_false 35 | 36 | em do 37 | @zkem.connect do 38 | @zkem.mkdir_p(@bogus_paths.first).callback do |p| 39 | p.first.should == @bogus_paths.first 40 | close_and_done! 41 | end.errback do |e| 42 | raise e 43 | end 44 | end 45 | end 46 | end 47 | 48 | it %[should not error on a path that already exists] do 49 | @zk.mkdir_p(@bogus_paths.first) 50 | 51 | em do 52 | @zkem.connect do 53 | @zkem.mkdir_p(@bogus_paths.first) do |exc,p| 54 | exc.should be_nil 55 | p.first.should == @bogus_paths.first 56 | close_and_done! 57 | end 58 | end 59 | end 60 | end 61 | 62 | it %[should take an array of paths] do 63 | @bogus_paths.each do |p| 64 | @zk.exists?(p).should be_false 65 | end 66 | 67 | em do 68 | @zkem.connect do 69 | @zkem.mkdir_p(@bogus_paths) do |exc,paths| 70 | exc.should be_nil 71 | paths.should be_kind_of(Array) 72 | @bogus_paths.each { |p| paths.should include(p) } 73 | close_and_done! 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | describe 'rm_rf' do 81 | em_before do 82 | @relpaths = ['disco/foo', 'prune/bar', 'fig/bar/one', 'apple/bar/two', 'orange/quux/c/d/e'] 83 | 84 | @roots = @relpaths.map { |p| File.join(@base_path, p.split('/').first) }.uniq 85 | @paths = @relpaths.map { |n| File.join(@base_path, n) } 86 | 87 | @paths.each { |n| @zk.mkdir_p(n) } 88 | end 89 | 90 | it %[should remove the paths recursively] do 91 | em do 92 | @zkem.connect do 93 | @zkem.rm_rf(@roots).callback do 94 | @roots.each { |p| @zk.exists?(p).should be_false } 95 | close_and_done! 96 | end.errback do |exc| 97 | raise exc 98 | end 99 | end 100 | end 101 | end # it 102 | 103 | it %[should use the nodejs style if a block is given] do 104 | em do 105 | @zkem.connect do 106 | @zkem.rm_rf(@roots) do |exc| 107 | if exc.nil? 108 | @roots.each { |p| @zk.exists?(p).should be_false } 109 | close_and_done! 110 | else 111 | raise exc 112 | end 113 | end 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## ZKEventMachine 2 | 3 | ZKEventMachine is a [ZK][] client implementation for [EventMachine][] for interacting with the Apache [ZooKeeper][] server. It provides the core functionality of [ZK][], but in a single-threaded context with a callback-based API. It is tested on [JRuby][], and [MRI][] versions 1.8.7 and 1.9.2. [Rubinius][] 1.2.x _should_ work, but support should be considered experimental at this point (if you find a bug, please [report it][], and I'll do my best to fix it). 4 | 5 | ### Quickstart 6 | 7 | Installation via rubygems is recommended, as there are a few dependencies. 8 | 9 | $ gem install zk-eventmachine 10 | 11 | This will install [ZK][] and [slyphon-zookeeper][] _(as a side note, for experimentation in irb, it's probably easier to use [ZK][] due to its synchronous nature)_. 12 | 13 | ### Connecting 14 | 15 | Connections are easy to set up, and take the same host string argument that the ZK and Zookeeper use. 16 | 17 | # a connection to a single server: 18 | 19 | zkem = ZK::ZKEventMachine::Client.new("localhost:2181") 20 | 21 | zkem.connect do 22 | # the client is connected when this block is called 23 | end 24 | 25 | _Note: at the moment, the [chroot-style][] syntax is iffy and needs some attention._ 26 | 27 | Closing a connection should be done in the same style, by passing a block to the _close_ method. 28 | 29 | zkem.close do 30 | # connection is closed when this block is called 31 | end 32 | 33 | Due to the way that the underlying [slyphon-zookeeper][] code is written, it is important that you not stop the reactor until the `on_close` callback has fired (especially when using `epoll` on linux). Strange things may happen if you do not wait for the connection to be closed! 34 | 35 | 36 | ### Callbacks 37 | 38 | ZKEventMachine was written so that every call can handle two callback styles. The first is node-js style: 39 | 40 | zkem.get('/') do |exception,value,stat| 41 | end 42 | 43 | In this style, the first value returned to the block is an Exception object if an error occured, or nil if the operation was successful. The rest of the arguments are the same as they would be returned from the synchronous API. 44 | 45 | The second style uses EventMachine::Deferrable (with a few slight modifications), and allows you to add callbacks and errbacks (in something approximating Twisted Python style). 46 | 47 | d = zkem.get('/') 48 | 49 | d.callback do |value,stat| 50 | # success 51 | end 52 | 53 | d.errback do |exc| 54 | # failure 55 | end 56 | 57 | The callback/errbacks return self, so you can chain calls: 58 | 59 | zkem.get('/').callback do |value,stat| 60 | 61 | end.errback do |exc| 62 | 63 | end 64 | 65 | Also provided is an `ensure_that` method that will add the given block to both callback and errback chains: 66 | 67 | # the goalposts |*| below are so that the block can take any number of 68 | # args, and ignore them 69 | 70 | zkem.get('/').ensure_that do |*| 71 | # clean up 72 | end 73 | 74 | ### Example Usage 75 | 76 | ### Contributing 77 | 78 | ### Credits 79 | 80 | ZKEventMachine is developed and maintained by Jonathan Simms and Topper Bowers. The HP Development Corp. has graciously open sourced this project under the MIT License, and special thanks go to [Snapfish][] who allowed us to develop this project. 81 | 82 | [ZK]: https://github.com/slyphon/zk 83 | [EventMachine]: https://github.com/eventmachine/eventmachine 84 | [ZooKeeper]: http://zookeeper.apache.org/ 85 | [slyphon-zookeeper]: https://github.com/slyphon/zookeeper 86 | [JRuby]: http://jruby.org 87 | [MRI]: http://www.ruby-lang.org/ 88 | [Rubinius]: http://rubini.us 89 | [report it]: https://github.com/slyphon/zk-eventmachine/issues 90 | [chroot-style]: http://zookeeper.apache.org/doc/r3.2.2/zookeeperProgrammers.html#ch_zkSessions 91 | [Snapfish]: http://www.snapfish.com 92 | 93 | -------------------------------------------------------------------------------- /spec/z_k/z_k_event_machine/callback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'ZK::ZKEventMachine::Callback' do 4 | before do 5 | @stat_mock = flexmock(:stat) 6 | @context_mock = flexmock(:context) 7 | 8 | flexmock(::EM) do |em| 9 | em.should_receive(:next_tick).with(Proc).and_return { |b| b.call } 10 | end 11 | 12 | end 13 | 14 | describe 'DataCallback' do 15 | before do 16 | @cb = ZK::ZKEventMachine::Callback::DataCallback.new 17 | end 18 | 19 | describe 'call' do 20 | describe 'with callbacks and errbacks set' do 21 | before do 22 | @callback_args = @errback_args = nil 23 | 24 | @cb.callback do |*a| 25 | @callback_args = a 26 | end 27 | 28 | @cb.errback do |*a| 29 | @errback_args = a 30 | end 31 | end 32 | 33 | describe 'success' do 34 | before do 35 | @cb.call(:rc => 0, :data => 'data', :stat => @stat_mock, :context => @context_mock) 36 | end 37 | 38 | it %[should have called the callback] do 39 | @callback_args.should_not be_nil 40 | end 41 | 42 | it %[should have the correct number of args] do 43 | @callback_args.length.should == 2 44 | end 45 | 46 | it %[should have the correct args] do 47 | @callback_args[0].should == 'data' 48 | @callback_args[1].should == @stat_mock 49 | end 50 | end 51 | 52 | describe 'failure' do 53 | before do 54 | @cb.call(:rc => ::ZK::Exceptions::NONODE) 55 | end 56 | 57 | it %[should have called the errback] do 58 | @errback_args.should_not be_nil 59 | end 60 | 61 | it %[should be called with the appropriate exception instance] do 62 | @errback_args.first.should be_instance_of(::ZK::Exceptions::NoNode) 63 | end 64 | end 65 | end 66 | 67 | describe 'with an on_result block set' do 68 | before do 69 | @args = nil 70 | 71 | @cb.on_result do |*a| 72 | @args = a 73 | end 74 | end 75 | 76 | describe 'success' do 77 | before do 78 | @cb.call(:rc => 0, :data => 'data', :stat => @stat_mock, :context => @context_mock) 79 | end 80 | 81 | it %[should have called the block] do 82 | @args.should_not be_nil 83 | end 84 | 85 | it %[should have used the correct arguments] do 86 | @args[0].should == nil 87 | @args[1].should == 'data' 88 | @args[2].should == @stat_mock 89 | end 90 | end 91 | 92 | describe 'failure' do 93 | before do 94 | @cb.call(:rc => ::ZK::Exceptions::NONODE) 95 | end 96 | 97 | it %[should have called the block] do 98 | @args.should_not be_nil 99 | end 100 | 101 | it %[should have used the correct arguments] do 102 | @args.first.should be_instance_of(::ZK::Exceptions::NoNode) 103 | end 104 | end 105 | end 106 | 107 | describe 'on_result can be handed a block' do 108 | before do 109 | @args = nil 110 | 111 | blk = lambda { |*a| @args = a } 112 | 113 | @cb.on_result(blk) 114 | end 115 | 116 | describe 'success' do 117 | before do 118 | @cb.call(:rc => 0, :data => 'data', :stat => @stat_mock, :context => @context_mock) 119 | end 120 | 121 | it %[should have called the block] do 122 | @args.should_not be_nil 123 | end 124 | 125 | it %[should have used the correct arguments] do 126 | @args[0].should == nil 127 | @args[1].should == 'data' 128 | @args[2].should == @stat_mock 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | 136 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/unixisms.rb: -------------------------------------------------------------------------------- 1 | module ZK 2 | module ZKEventMachine 3 | module Unixisms 4 | def mkdir_p(paths, &block) 5 | dfr = Deferred::Default.new.tap do |my_dfr| 6 | Iterator.new(Array(paths).flatten.compact, 1).map( 7 | lambda { |path,iter| # foreach 8 | d = _mkdir_p_dfr(path) 9 | d.callback { |p| iter.return(p) } 10 | d.errback { |e| my_dfr.fail(e) } 11 | }, 12 | lambda { |results| my_dfr.succeed(results) } # after completion 13 | ) 14 | end 15 | 16 | _handle_calling_convention(dfr, &block) 17 | end 18 | 19 | def rm_rf(paths, &blk) 20 | dfr = Deferred::Default.new.tap do |my_dfr| 21 | Iterator.new(Array(paths).flatten.compact, 1).each( 22 | lambda { |path,iter| # foreach 23 | d = _rm_rf_dfr(path) 24 | d.callback { iter.next } 25 | d.errback { |e| my_dfr.fail(e) } 26 | }, 27 | lambda { my_dfr.succeed } # after completion 28 | ) 29 | end 30 | 31 | _handle_calling_convention(dfr, &blk) 32 | end 33 | 34 | # @private 35 | def find(*paths, &block) 36 | raise NotImplementedError, "Coming soon" 37 | end 38 | 39 | # @private 40 | def block_until_node_deleted(abs_node_path) 41 | raise NotImplementedError, "blocking does not make sense in EventMachine-land" 42 | end 43 | 44 | protected 45 | # @private 46 | def _handle_calling_convention(dfr, &blk) 47 | return dfr unless blk 48 | dfr.callback { |*a| blk.call(nil, *a) } 49 | dfr.errback { |exc| blk.call(exc) } 50 | dfr 51 | end 52 | 53 | # @private 54 | def _rm_rf_dfr(path) 55 | Deferred::Default.new.tap do |my_dfr| 56 | delete(path) do |exc| 57 | case exc 58 | when nil, Exceptions::NoNode 59 | my_dfr.succeed 60 | when Exceptions::NotEmpty 61 | children(path) do |exc,chldrn,_| 62 | case exc 63 | when Exceptions::NoNode 64 | my_dfr.succeed 65 | when nil 66 | abspaths = chldrn.map { |n| [path, n].join('/') } 67 | Iterator.new(abspaths).each( 68 | lambda { |absp,iter| 69 | d = _rm_rf_dfr(absp) 70 | d.callback { |*| 71 | logger.debug { "removed #{absp}" } 72 | iter.next 73 | } 74 | d.errback { |e| 75 | logger.debug { "got failure #{e.inspect}" } 76 | my_dfr.fail(e) # this will stop the iteration 77 | } 78 | }, 79 | lambda { 80 | my_dfr.chain_to(_rm_rf_dfr(path)) 81 | } 82 | ) 83 | else 84 | my_dfr.fail(exc) 85 | end 86 | end 87 | end 88 | end 89 | end 90 | end 91 | 92 | # @private 93 | def _mkdir_p_dfr(path) 94 | Deferred::Default.new.tap do |my_dfr| 95 | d = create(path, '') 96 | 97 | d.callback do |new_path| 98 | my_dfr.succeed(new_path) 99 | end 100 | 101 | d.errback do |exc| 102 | case exc 103 | when Exceptions::NodeExists 104 | # this is the bottom of the stack, where we start bubbling back up 105 | # or the first call, path already exists, return 106 | my_dfr.succeed(path) 107 | when Exceptions::NoNode 108 | # our node didn't exist now, so we try an recreate it after our 109 | # parent has been created 110 | 111 | parent_d = mkdir_p(File.dirname(path)) # set up our parent to be created 112 | 113 | parent_d.callback do |parent_path| # once our parent exists 114 | create(path, '') do |exc,p| # create our path again 115 | exc ? my_dfr.fail(exc) : my_dfr.succeed(p) # pass our success or failure up the chain 116 | end 117 | end 118 | 119 | parent_d.errback do |e| # if creating our parent fails 120 | my_dfr.fail(e) # pass that along too 121 | end 122 | end 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | 130 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/iterator.rb: -------------------------------------------------------------------------------- 1 | # Taken from EventMachine release candidate 2 | module ZK 3 | module ZKEventMachine 4 | # A simple iterator for concurrent asynchronous work. 5 | # 6 | # Unlike ruby's built-in iterators, the end of the current iteration cycle is signaled manually, 7 | # instead of happening automatically after the yielded block finishes executing. For example: 8 | # 9 | # (0..10).each{ |num| } 10 | # 11 | # becomes: 12 | # 13 | # EM::Iterator.new(0..10).each{ |num,iter| iter.next } 14 | # 15 | # This is especially useful when doing asynchronous work via reactor libraries and 16 | # functions. For example, given a sync and async http api: 17 | # 18 | # response = sync_http_get(url); ... 19 | # async_http_get(url){ |response| ... } 20 | # 21 | # a synchronous iterator such as: 22 | # 23 | # responses = urls.map{ |url| sync_http_get(url) } 24 | # ... 25 | # puts 'all done!' 26 | # 27 | # could be written as: 28 | # 29 | # EM::Iterator.new(urls).map(proc{ |url,iter| 30 | # async_http_get(url){ |res| 31 | # iter.return(res) 32 | # } 33 | # }, proc{ |responses| 34 | # ... 35 | # puts 'all done!' 36 | # }) 37 | # 38 | # Now, you can take advantage of the asynchronous api to issue requests in parallel. For example, 39 | # to fetch 10 urls at a time, simply pass in a concurrency of 10: 40 | # 41 | # EM::Iterator.new(urls, 10).each do |url,iter| 42 | # async_http_get(url){ iter.next } 43 | # end 44 | # 45 | class Iterator 46 | # Create a new parallel async iterator with specified concurrency. 47 | # 48 | # i = EM::Iterator.new(1..100, 10) 49 | # 50 | # will create an iterator over the range that processes 10 items at a time. Iteration 51 | # is started via #each, #map or #inject 52 | # 53 | def initialize(list, concurrency = 1) 54 | raise ArgumentError, 'argument must be an array' unless list.respond_to?(:to_a) 55 | @list = list.to_a.dup 56 | @concurrency = concurrency 57 | 58 | @started = false 59 | @ended = false 60 | end 61 | 62 | # Change the concurrency of this iterator. Workers will automatically be spawned or destroyed 63 | # to accomodate the new concurrency level. 64 | # 65 | def concurrency=(val) 66 | old = @concurrency 67 | @concurrency = val 68 | 69 | spawn_workers if val > old and @started and !@ended 70 | end 71 | attr_reader :concurrency 72 | 73 | # Iterate over a set of items using the specified block or proc. 74 | # 75 | # EM::Iterator.new(1..100).each do |num, iter| 76 | # puts num 77 | # iter.next 78 | # end 79 | # 80 | # An optional second proc is invoked after the iteration is complete. 81 | # 82 | # EM::Iterator.new(1..100).each( 83 | # proc{ |num,iter| iter.next }, 84 | # proc{ puts 'all done' } 85 | # ) 86 | # 87 | def each(foreach=nil, after=nil, &blk) 88 | raise ArgumentError, 'proc or block required for iteration' unless foreach ||= blk 89 | raise RuntimeError, 'cannot iterate over an iterator more than once' if @started or @ended 90 | 91 | @started = true 92 | @pending = 0 93 | @workers = 0 94 | 95 | all_done = proc{ 96 | after.call if after and @ended and @pending == 0 97 | } 98 | 99 | @process_next = proc{ 100 | # p [:process_next, :pending=, @pending, :workers=, @workers, :ended=, @ended, :concurrency=, @concurrency, :list=, @list] 101 | unless @ended or @workers > @concurrency 102 | if @list.empty? 103 | @ended = true 104 | @workers -= 1 105 | all_done.call 106 | else 107 | item = @list.shift 108 | @pending += 1 109 | 110 | is_done = false 111 | on_done = proc{ 112 | raise RuntimeError, 'already completed this iteration' if is_done 113 | is_done = true 114 | 115 | @pending -= 1 116 | 117 | if @ended 118 | all_done.call 119 | else 120 | EM.next_tick(@process_next) 121 | end 122 | } 123 | class << on_done 124 | alias :next :call 125 | end 126 | 127 | foreach.call(item, on_done) 128 | end 129 | else 130 | @workers -= 1 131 | end 132 | } 133 | 134 | spawn_workers 135 | 136 | self 137 | end 138 | 139 | # Collect the results of an asynchronous iteration into an array. 140 | # 141 | # EM::Iterator.new(%w[ pwd uptime uname date ], 2).map(proc{ |cmd,iter| 142 | # EM.system(cmd){ |output,status| 143 | # iter.return(output) 144 | # } 145 | # }, proc{ |results| 146 | # p results 147 | # }) 148 | # 149 | def map(foreach, after) 150 | index = 0 151 | 152 | inject([], proc{ |results,item,iter| 153 | i = index 154 | index += 1 155 | 156 | is_done = false 157 | on_done = proc{ |res| 158 | raise RuntimeError, 'already returned a value for this iteration' if is_done 159 | is_done = true 160 | 161 | results[i] = res 162 | iter.return(results) 163 | } 164 | class << on_done 165 | alias :return :call 166 | def next 167 | raise NoMethodError, 'must call #return on a map iterator' 168 | end 169 | end 170 | 171 | foreach.call(item, on_done) 172 | }, proc{ |results| 173 | after.call(results) 174 | }) 175 | end 176 | 177 | # Inject the results of an asynchronous iteration onto a given object. 178 | # 179 | # EM::Iterator.new(%w[ pwd uptime uname date ], 2).inject({}, proc{ |hash,cmd,iter| 180 | # EM.system(cmd){ |output,status| 181 | # hash[cmd] = status.exitstatus == 0 ? output.strip : nil 182 | # iter.return(hash) 183 | # } 184 | # }, proc{ |results| 185 | # p results 186 | # }) 187 | # 188 | def inject(obj, foreach, after) 189 | each(proc{ |item,iter| 190 | is_done = false 191 | on_done = proc{ |res| 192 | raise RuntimeError, 'already returned a value for this iteration' if is_done 193 | is_done = true 194 | 195 | obj = res 196 | iter.next 197 | } 198 | class << on_done 199 | alias :return :call 200 | def next 201 | raise NoMethodError, 'must call #return on an inject iterator' 202 | end 203 | end 204 | 205 | foreach.call(obj, item, on_done) 206 | }, proc{ 207 | after.call(obj) 208 | }) 209 | end 210 | 211 | private 212 | 213 | # Spawn workers to consume items from the iterator's enumerator based on the current concurrency level. 214 | # 215 | def spawn_workers 216 | EM.next_tick(start_worker = proc{ 217 | if @workers < @concurrency and !@ended 218 | # p [:spawning_worker, :workers=, @workers, :concurrency=, @concurrency, :ended=, @ended] 219 | @workers += 1 220 | @process_next.call 221 | EM.next_tick(start_worker) 222 | end 223 | }) 224 | nil 225 | end 226 | end 227 | end 228 | end 229 | 230 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/callback.rb: -------------------------------------------------------------------------------- 1 | module ZK 2 | module ZKEventMachine 3 | # some improvements (one hopes) around the zookeeper gem's somewhat (ahem) 4 | # minimal Callback class 5 | # 6 | module Callback 7 | 8 | # Used by ZooKeeper to return an asynchronous result. 9 | # 10 | # If callbacks or errbacks are set on the instance, they will be called 11 | # with just the data returned from the call (much like their synchronous 12 | # versions). 13 | # 14 | # If a block was given to #new or #on_result, then that block is called 15 | # with a ZK::Exceptions::KeeperException instance or nil, then the rest 16 | # of the arguments defined for that callback type 17 | # 18 | # the node-style and deferred-style results are *NOT* exclusive, so if 19 | # you use both _you will be called with results in both formats_. 20 | # 21 | class Base 22 | include Deferred 23 | include ZK::Logging 24 | 25 | # set the result keys that should be used by node_style_result and to 26 | # call the deferred_style_result blocks 27 | # 28 | def self.async_result_keys(*syms) 29 | if syms.empty? 30 | @async_result_keys || [] 31 | else 32 | @async_result_keys = syms.map { |n| n.to_sym } 33 | end 34 | end 35 | 36 | # save a context object, used for associating delivered events with the request that created them 37 | attr_accessor :context 38 | 39 | # saves the request id of this call 40 | attr_reader :req_id 41 | 42 | def initialize(prok=nil, &block) 43 | on_result(prok, &block) 44 | end 45 | 46 | # register a block that should be called (node.js style) with the 47 | # results 48 | # 49 | # @note replaces the block given to #new 50 | # 51 | def on_result(prok=nil, &block) 52 | @block = (prok || block) 53 | end 54 | 55 | # Checks the return code from the async call. If the return code was not ZOK, 56 | # then fire the errbacks and do the node-style error call 57 | # otherwise, does nothing 58 | # 59 | # in this call we also stash the outgoing req_id so we can sync it up 60 | def check_async_rc(hash) 61 | @req_id = hash[:req_id] 62 | logger.debug { "#{__method__}: got #{hash.inspect}" } 63 | call(hash) unless success?(hash) 64 | end 65 | 66 | # ZK will call this instance with a hash of data, which is the result 67 | # of the asynchronous call. Depending on the style of callback in use, 68 | # we take the appropriate actions 69 | # 70 | # delegates to #deferred_style_result and #node_style_result 71 | def call(result) 72 | logger.debug { "\n#{self.class.name}##{__method__}\n\treq_id: #{req_id.inspect}\n\tcontext: #{context.inspect}\n\tresult: #{result.inspect}" } 73 | EM.schedule do 74 | deferred_style_result(result) 75 | node_style_result(result) 76 | end 77 | end 78 | 79 | # returns true if the request was successful (if return_code was Zookeeper::ZOK) 80 | # 81 | # @param [Hash] hash the result of the async call 82 | # 83 | # @return [true, false] for success, failure 84 | def success?(hash) 85 | hash[:rc] == Zookeeper::ZOK 86 | end 87 | 88 | # Returns an instance of a sublcass ZK::Exceptions::KeeperException 89 | # based on the asynchronous return_code. 90 | # 91 | # facilitates using case statements for error handling 92 | # 93 | # @param [Hash] hash the result of the async call 94 | # 95 | # @raise [RuntimeError] if the return_code is not known by ZK (this should never 96 | # happen and if it does, you should report a bug) 97 | # 98 | # @return [ZK::Exceptions::KeeperException, nil] subclass based on 99 | # return_code if there was an error, nil otherwise 100 | # 101 | def exception_for(hash) 102 | return nil if success?(hash) 103 | return_code = hash.fetch(:rc) 104 | ZK::Exceptions::KeeperException.by_code(return_code).new 105 | end 106 | 107 | # @abstract should call set_deferred_status with the appropriate args 108 | # for the result and type of call 109 | def deferred_style_result(hash) 110 | # ensure this calls the callback on the reactor 111 | 112 | if success?(hash) 113 | vals = hash.values_at(*async_result_keys) 114 | # logger.debug { "#{self.class.name}#deferred_style_result async_result_keys: #{async_result_keys.inspect}, vals: #{vals.inspect}" } 115 | self.succeed(*hash.values_at(*async_result_keys)) 116 | else 117 | self.fail(exception_for(hash)) 118 | end 119 | end 120 | 121 | # call the user block with the correct Exception class as the first arg 122 | # (or nil if no error) and then the appropriate args for the type of 123 | # asynchronous call 124 | def node_style_result(hash) 125 | return unless @block 126 | vals = hash.values_at(*async_result_keys) 127 | # logger.debug { "#{self.class.name}#node_style_result async_result_keys: #{async_result_keys.inspect}, vals: #{vals.inspect}" } 128 | if exc = exception_for(hash) 129 | @block.call(exc) 130 | else 131 | @block.call(nil, *vals) 132 | end 133 | end 134 | 135 | protected 136 | def async_result_keys 137 | self.class.async_result_keys 138 | end 139 | end 140 | 141 | # used with Client#get call 142 | class DataCallback < Base 143 | async_result_keys :data, :stat 144 | end 145 | 146 | # used with Client#children call 147 | class ChildrenCallback < Base 148 | async_result_keys :strings, :stat 149 | end 150 | 151 | # used with Client#create 152 | class StringCallback < Base 153 | async_result_keys :string 154 | end 155 | 156 | # used with Client#stat and Client#exists? 157 | class StatCallback < Base 158 | async_result_keys :stat 159 | 160 | # stat has a different concept of 'success', stat on a node that doesn't 161 | # exist is not an exception, it's a certain kind of stat (like a null stat). 162 | def success?(hash) 163 | rc = hash[:rc] 164 | (rc == Zookeeper::ZOK) || (rc == Zookeeper::ZNONODE) 165 | end 166 | end 167 | 168 | # supports the syntactic sugar exists? call 169 | class ExistsCallback < StatCallback 170 | async_result_keys :stat 171 | 172 | # @abstract should call set_deferred_status with the appropriate args 173 | # for the result and type of call 174 | def deferred_style_result(hash) 175 | # ensure this calls the callback on the reactor 176 | 177 | if success?(hash) 178 | succeed(hash[:stat].exists?) 179 | else 180 | fail(exception_for(hash)) 181 | end 182 | end 183 | 184 | # call the user block with the correct Exception class as the first arg 185 | # (or nil if no error) and then the appropriate args for the type of 186 | # asynchronous call 187 | def node_style_result(hash) 188 | return unless @block 189 | @block.call(exception_for(hash), hash[:stat].exists?) 190 | end 191 | end 192 | 193 | # set operation returns a stat object, but in this case a NONODE is an 194 | # error (unlike with StatCallback) 195 | class SetCallback < Base 196 | async_result_keys :stat 197 | end 198 | 199 | # used with Client#delete and Client#set_acl 200 | class VoidCallback < Base 201 | end 202 | 203 | # used with Client#get_acl 204 | class ACLCallback < Base 205 | async_result_keys :acl, :stat 206 | end 207 | 208 | class << self 209 | def new_data_cb(njs_block) 210 | DataCallback.new(njs_block).tap do |cb| # create the callback with the user-provided block 211 | cb.check_async_rc(yield(cb)) # yield the callback to the caller, check the return result 212 | # of the async operation (not the async result) 213 | end # return the callback 214 | end 215 | alias :new_get_cb :new_data_cb # create alias so that this matches the client API name 216 | 217 | def new_string_cb(njs_block) 218 | StringCallback.new(njs_block).tap do |cb| 219 | cb.check_async_rc(yield(cb)) 220 | end 221 | end 222 | alias :new_create_cb :new_string_cb 223 | 224 | def new_stat_cb(njs_block) 225 | StatCallback.new(njs_block).tap do |cb| 226 | cb.check_async_rc(yield(cb)) 227 | end 228 | end 229 | 230 | def new_exists_cb(njs_block) 231 | ExistsCallback.new(njs_block).tap do |cb| 232 | cb.check_async_rc(yield(cb)) 233 | end 234 | end 235 | 236 | def new_set_cb(njs_block) 237 | SetCallback.new(njs_block).tap do |cb| 238 | cb.check_async_rc(yield(cb)) 239 | end 240 | end 241 | 242 | def new_void_cb(njs_block) 243 | VoidCallback.new(njs_block).tap do |cb| 244 | cb.check_async_rc(yield(cb)) 245 | end 246 | end 247 | alias :new_delete_cb :new_void_cb 248 | alias :new_set_acl_cb :new_void_cb 249 | 250 | def new_children_cb(njs_block) 251 | ChildrenCallback.new(njs_block).tap do |cb| 252 | cb.check_async_rc(yield(cb)) 253 | end 254 | end 255 | 256 | def new_acl_cb(njs_block) 257 | ACLCallback.new(njs_block).tap do |cb| 258 | cb.check_async_rc(yield(cb)) 259 | end 260 | end 261 | alias :new_get_acl_cb :new_acl_cb 262 | end 263 | end 264 | end 265 | end 266 | 267 | -------------------------------------------------------------------------------- /lib/z_k/z_k_event_machine/client.rb: -------------------------------------------------------------------------------- 1 | module ZK 2 | module ZKEventMachine 3 | # @example use of on_connecting 4 | # 5 | # def handle_connecting_event(event=nil) 6 | # 7 | # # this (re-)registers this hook for the next time this event is called 8 | # @zkem.on_connecting(&:handle_connecting_event) 9 | # 10 | # return unless event # nil is the initial registration case 11 | # 12 | # logger.warn { "Oh no! got a connecting event, taking evasive action!" } 13 | # 14 | # # do stuff 15 | # end 16 | # 17 | # 18 | # # your setup method would then look like 19 | # def run 20 | # @zkem.connect do 21 | # handle_connecting_event 22 | # 23 | # do_the_rest_of_your_stuff 24 | # end 25 | # end 26 | # 27 | class Client < ZK::Client::Base 28 | include Deferred::Accessors 29 | include ZK::Logging 30 | include Unixisms 31 | 32 | DEFAULT_TIMEOUT = 10 33 | 34 | # If we get a ZK::Exceptions::ConnectionLoss exeption back from any call, 35 | # or a EXPIRED_SESSION_STATE event, we will call back any handlers registered 36 | # here with the exception instance as the argument. 37 | # 38 | # once this deferred has been fired, it will be replaced with a new 39 | # deferred, so callbacks must be re-registered, and *should* be 40 | # re-registered *within* the callback to avoid missing events 41 | # 42 | # @note if you want to be notified when the connection state has not 43 | # become *invalid* but you are in a possibly recoverable state, then 44 | # you should hook the {#on_connecting} method 45 | # 46 | # @method on_connection_lost 47 | # @return [Deferred::Default] 48 | deferred_event :connection_lost 49 | 50 | # Registers a one-shot callback for the ZOO_CONNECTED_STATE event. 51 | # 52 | # @note this is experimental currently. This may or may not fire for the *initial* connection. 53 | # it's purpose is to warn an already-existing client with watches that a connection has been 54 | # re-established (with session information saved). From the ZooKeeper Programmers' Guide: 55 | # 56 | # If you are using watches, you must look for the connected watch event. 57 | # When a ZooKeeper client disconnects from a server, you will not receive 58 | # notification of changes until reconnected. If you are watching for a 59 | # znode to come into existance, you will miss the event if the znode is 60 | # created and deleted while you are disconnected. 61 | # 62 | # once this deferred has been fired, it will be replaced with a new 63 | # deferred, so callbacks must be re-registered, and *should* be 64 | # re-registered *within* the callback to avoid missing events 65 | # 66 | # @method on_connected 67 | # @return [Deferred::Default] 68 | deferred_event :connected 69 | 70 | # Registers a one-shot callback for the ZOO_CONNECTING_STATE event 71 | # 72 | # This event is triggered when we have become disconnected from the 73 | # cluster and are in the process of reconnecting. 74 | # 75 | # @note this would more accurately be called `on_disconnection`, but because 76 | # it's fired also when the client is starting up, it has the name it does. 77 | # there's an alias for this `on_disconnection`. (It's not `on_disconnected` 78 | # because the '-ion' seems to indicate that the condition may be temporary) 79 | # 80 | # @method on_connecting 81 | # @return [Deferred::Default] 82 | deferred_event :connecting 83 | 84 | alias :on_disconnection :on_connecting 85 | 86 | # called back once the connection has been closed. 87 | # 88 | # @method on_close 89 | # @return [Deferred::Default] 90 | deferred_event :close 91 | 92 | # Takes same options as ZK::Client::Base 93 | def initialize(host, opts={}) 94 | @host = host 95 | @event_handler = EventHandlerEM.new(self) 96 | @closing = false 97 | register_default_event_handlers! 98 | end 99 | 100 | # @private 101 | def closing? 102 | !!@closing 103 | end 104 | 105 | # open a ZK connection, attach it to the reactor. 106 | # returns an EM::Deferrable that will be called when the connection is 107 | # ready for use 108 | def connect(&blk) 109 | # XXX: maybe move this into initialize, need to figure out how to schedule it properly 110 | @cnx ||= ( 111 | ZookeeperEM::Client.new(@host, DEFAULT_TIMEOUT, event_handler.get_default_watcher_block) 112 | ) 113 | @cnx.on_attached(&blk) 114 | end 115 | 116 | # @private 117 | def reopen(*a) 118 | raise NotImplementedError, "reoopen is not implemented for the eventmachine version of the client" 119 | end 120 | 121 | def close!(&blk) 122 | on_close(&blk) 123 | return on_close if @closing 124 | @closing = true 125 | 126 | if @cnx 127 | logger.debug { "#{self.class.name}: in close! clearing event_handler" } 128 | event_handler.clear! 129 | 130 | logger.debug { "#{self.class.name}: calling @cnx.close" } 131 | @cnx.close do 132 | logger.debug { "firing on_close handler" } 133 | on_close.succeed 134 | @cnx = nil 135 | end 136 | else 137 | on_close.succeed 138 | end 139 | 140 | on_close 141 | end 142 | alias :close :close! 143 | 144 | # get data at path, optionally enabling a watch on the node 145 | # 146 | # @return [Callback] returns a Callback which is an EM::Deferred (so you 147 | # can assign callbacks/errbacks) see Callback::Base for discussion 148 | # 149 | def get(path, opts={}, &block) 150 | Callback.new_get_cb(block) do |cb| 151 | cb.errback(&method(:connection_lost_hook)) 152 | cb.context = { :method => __method__, :path => path, :opts => opts } 153 | super(path, opts.merge(:callback => cb)) 154 | end 155 | end 156 | 157 | def create(path, data='', opts={}, &block) 158 | Callback.new_create_cb(block) do |cb| 159 | cb.errback(&method(:connection_lost_hook)) 160 | cb.context = { :method => __method__, :path => path, :data => data, :opts => opts } 161 | super(path, data, opts.merge(:callback => cb)) 162 | end 163 | end 164 | 165 | def set(path, data, opts={}, &block) 166 | Callback.new_set_cb(block) do |cb| 167 | cb.errback(&method(:connection_lost_hook)) 168 | cb.context = { :method => __method__, :path => path, :data => data, :opts => opts } 169 | super(path, data, opts.merge(:callback => cb)) 170 | end 171 | end 172 | 173 | def stat(path, opts={}, &block) 174 | cb_style = opts.delete(:cb_style) { |_| 'stat' } 175 | 176 | meth = :"new_#{cb_style}_cb" 177 | 178 | Callback.__send__(meth, block) do |cb| 179 | cb.errback(&method(:connection_lost_hook)) 180 | cb.context = { :method => __method__, :path => path, :meth => meth, :opts => opts } 181 | super(path, opts.merge(:callback => cb)) 182 | end 183 | end 184 | 185 | def exists?(path, opts={}, &block) 186 | stat(path, opts.merge(:cb_style => 'exists'), &block) 187 | end 188 | 189 | def delete(path, opts={}, &block) 190 | Callback.new_delete_cb(block) do |cb| 191 | cb.errback(&method(:connection_lost_hook)) 192 | cb.context = { :method => __method__, :path => path, :opts => opts } 193 | super(path, opts.merge(:callback => cb)) 194 | end 195 | end 196 | 197 | def children(path, opts={}, &block) 198 | Callback.new_children_cb(block) do |cb| 199 | cb.errback(&method(:connection_lost_hook)) 200 | cb.context = { :method => __method__, :path => path, :opts => opts } 201 | super(path, opts.merge(:callback => cb)) 202 | end 203 | end 204 | 205 | def get_acl(path, opts={}, &block) 206 | Callback.new_get_acl_cb(block) do |cb| 207 | cb.errback(&method(:connection_lost_hook)) 208 | cb.context = { :method => __method__, :path => path, :opts => opts } 209 | super(path, opts.merge(:callback => cb)) 210 | end 211 | end 212 | 213 | def set_acl(path, acls, opts={}, &block) 214 | Callback.new_set_acl_cb(block) do |cb| 215 | cb.errback(&method(:connection_lost_hook)) 216 | cb.context = { :method => __method__, :path => path, :acls => acls, :opts => opts } 217 | super(path, acls, opts.merge(:callback => cb)) 218 | end 219 | end 220 | 221 | # @return [Fixnum] The underlying connection's session_id 222 | def session_id 223 | return nil unless @cnx 224 | @cnx.session_id 225 | end 226 | 227 | # @return [String] The underlying connection's session passwd (an opaque value) 228 | def session_passwd 229 | return nil unless @cnx 230 | @cnx.session_passwd 231 | end 232 | 233 | # Used by the event_handler to deliver callbacks. This hopefully won't 234 | # have any other side-effects, as it's not happening on a separate 235 | # thread, rather just in the next reactor tick. 236 | # 237 | # @private 238 | def defer 239 | EM.next_tick { yield } 240 | end 241 | 242 | protected 243 | # @private 244 | def register_default_event_handlers! 245 | @event_handler.register_state_handler(Zookeeper::ZOO_EXPIRED_SESSION_STATE, &method(:handle_expired_session_state_event!)) 246 | @event_handler.register_state_handler(Zookeeper::ZOO_CONNECTED_STATE, &method(:handle_connected_state_event!)) 247 | @event_handler.register_state_handler(Zookeeper::ZOO_CONNECTING_STATE, &method(:handle_connecting_state_event!)) 248 | end 249 | 250 | # @private 251 | def handle_connected_state_event!(event) 252 | EM.schedule { reset_connected_event.succeed(event) } 253 | end 254 | 255 | # @private 256 | def handle_connecting_state_event!(event) 257 | EM.schedule { reset_connecting_event.succeed(event) } 258 | end 259 | 260 | # @private 261 | def handle_expired_session_state_event!(event) 262 | exc = ZK::Exceptions::ConnectionLoss.new("Received EXPIRED_SESSION_STATE event: #{event.inspect}") 263 | exc.set_backtrace(caller) 264 | connection_lost_hook(exc) 265 | end 266 | 267 | # @private 268 | def connection_lost_hook(exc) 269 | if exc and exc.kind_of?(ZK::Exceptions::ConnectionLoss) 270 | EM.schedule { reset_connection_lost_event.succeed(exc) } 271 | end 272 | end 273 | end 274 | end 275 | end 276 | 277 | -------------------------------------------------------------------------------- /spec/z_k/z_k_event_machine/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module ZK::ZKEventMachine 4 | shared_examples_for 'Client' do 5 | include EventedSpec::SpecHelper 6 | default_timeout 2.0 7 | 8 | let(:base_path) { '/zk-em-testing' } 9 | 10 | before do 11 | @zk.rm_rf(base_path) 12 | @zk.mkdir_p(base_path) 13 | end 14 | 15 | after do 16 | @zk.rm_rf(base_path) 17 | @zk.close! 18 | end 19 | 20 | def event_mock(name=:event) 21 | flexmock(name).tap do |ev| 22 | ev.should_receive(:node_event?).and_return(false) 23 | ev.should_receive(:state_event?).and_return(true) 24 | ev.should_receive(:session_event?).and_return(true) 25 | ev.should_receive(:zk=).with_any_args 26 | yield ev if block_given? 27 | end 28 | end 29 | 30 | describe 'connect' do 31 | it %[should return a deferred that fires when connected and then close] do 32 | em do 33 | @zkem.connect do 34 | true.should be_true 35 | @zkem.close! do 36 | logger.debug { "calling done" } 37 | done 38 | end 39 | end 40 | end 41 | end 42 | 43 | it %[should be able to be called mulitple times] do 44 | em do 45 | @zkem.connect do 46 | logger.debug { "inside first callback" } 47 | @zkem.connect do 48 | logger.debug { "inside second callback" } 49 | true.should be_true 50 | @zkem.close! { done } 51 | end 52 | end 53 | end 54 | end 55 | end 56 | 57 | describe 'get' do 58 | describe 'success' do 59 | before do 60 | @path = [base_path, 'foo'].join('/') 61 | @data = 'this is data' 62 | @zk.create(@path, @data) 63 | end 64 | 65 | it 'should get the data and call the callback' do 66 | em do 67 | @zkem.connect do 68 | dfr = @zkem.get(@path) 69 | 70 | dfr.callback do |*a| 71 | logger.debug { "got callback with #{a.inspect}" } 72 | a.should_not be_empty 73 | a.first.should == @data 74 | a.last.should be_instance_of(Zookeeper::Stat) 75 | EM.reactor_thread?.should be_true 76 | @zkem.close! { done } 77 | end 78 | 79 | dfr.errback do |exc| 80 | raise exc 81 | end 82 | end 83 | end 84 | end 85 | 86 | it 'should get the data and do a nodejs-style callback' do 87 | em do 88 | @zkem.connect do 89 | @zkem.get(@path) do |exc,data,stat| 90 | exc.should be_nil 91 | data.should == @data 92 | stat.should be_instance_of(Zookeeper::Stat) 93 | EM.reactor_thread?.should be_true 94 | @zkem.close! { done } 95 | end 96 | end 97 | end 98 | end 99 | end # success 100 | 101 | describe 'failure' do 102 | before do 103 | @path = [base_path, 'foo'].join('/') 104 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 105 | end 106 | 107 | it %[should call the errback in deferred style] do 108 | em do 109 | @zkem.connect do 110 | d = @zkem.get(@path) 111 | 112 | d.callback do 113 | raise "Should not have been called" 114 | end 115 | 116 | d.errback do |exc| 117 | exc.should be_kind_of(ZK::Exceptions::NoNode) 118 | logger.debug { "calling done" } 119 | @zkem.close! { done } 120 | end 121 | end 122 | end 123 | end 124 | 125 | it %[should have NoNode as the first argument to the block] do 126 | em do 127 | @zkem.connect do 128 | @zkem.get(@path) do |exc,*a| 129 | exc.should be_kind_of(ZK::Exceptions::NoNode) 130 | @zkem.close! { done } 131 | end 132 | end 133 | end 134 | end 135 | end # failure 136 | end # get 137 | 138 | describe 'create' do 139 | describe 'success' do 140 | before do 141 | @path = [base_path, 'foo'].join('/') 142 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 143 | 144 | @data = 'this is data' 145 | end 146 | 147 | describe 'non-sequence node' do 148 | it 'should create the node and call the callback' do 149 | em do 150 | @zkem.connect do 151 | d = @zkem.create(@path, @data) 152 | 153 | d.callback do |*a| 154 | logger.debug { "got callback with #{a.inspect}" } 155 | a.should_not be_empty 156 | a.first.should == @path 157 | EM.reactor_thread?.should be_true 158 | @zkem.close! { done } 159 | end 160 | 161 | d.errback do |exc| 162 | raise exc 163 | end 164 | end 165 | end 166 | end 167 | 168 | it 'should get the data and do a nodejs-style callback' do 169 | em do 170 | @zkem.connect do 171 | @zkem.create(@path, @data) do |exc,created_path| 172 | exc.should be_nil 173 | created_path.should == @path 174 | EM.reactor_thread?.should be_true 175 | @zkem.close! { done } 176 | end 177 | end 178 | end 179 | end 180 | end # non-sequence node 181 | 182 | describe 'sequence node' do 183 | it 'should create the node and call the callback' do 184 | em do 185 | @zkem.connect do 186 | d = @zkem.create(@path, @data, :sequence => true) 187 | 188 | d.callback do |*a| 189 | logger.debug { "got callback with #{a.inspect}" } 190 | a.should_not be_empty 191 | a.first.should =~ /#{@path}\d+$/ 192 | EM.reactor_thread?.should be_true 193 | @zkem.close! { done } 194 | end 195 | 196 | d.errback do |exc| 197 | raise exc 198 | end 199 | end 200 | end 201 | end 202 | end 203 | end # success 204 | 205 | describe 'failure' do 206 | before do 207 | @path = [base_path, 'foo'].join('/') 208 | @zk.create(@path, '') 209 | end 210 | 211 | it %[should call the errback in deferred style] do 212 | em do 213 | @zkem.connect do 214 | d = @zkem.create(@path, '') 215 | 216 | d.callback do 217 | raise "Should not have been called" 218 | end 219 | 220 | d.errback do |exc| 221 | exc.should be_kind_of(ZK::Exceptions::NodeExists) 222 | @zkem.close! { done } 223 | end 224 | end 225 | end 226 | end 227 | 228 | it %[should have exception as the first argument to the block] do 229 | em do 230 | @zkem.connect do 231 | @zkem.create(@path, '') do |exc,*a| 232 | exc.should be_kind_of(ZK::Exceptions::NodeExists) 233 | @zkem.close! { done } 234 | end 235 | end 236 | end 237 | end 238 | end # failure 239 | end # create 240 | 241 | describe 'set' do 242 | describe 'success' do 243 | before do 244 | @path = [base_path, 'foo'].join('/') 245 | @data = 'this is data' 246 | @new_data = 'this is better data' 247 | @zk.create(@path, @data) 248 | @orig_stat = @zk.stat(@path) 249 | end 250 | 251 | it 'should set the data and call the callback' do 252 | em do 253 | @zkem.connect do 254 | dfr = @zkem.set(@path, @new_data) 255 | 256 | dfr.callback do |stat| 257 | stat.should be_instance_of(Zookeeper::Stat) 258 | stat.version.should > @orig_stat.version 259 | EM.reactor_thread?.should be_true 260 | 261 | @zkem.get(@path) do |_,data| 262 | data.should == @new_data 263 | @zkem.close! { done } 264 | end 265 | end 266 | 267 | dfr.errback do |exc| 268 | raise exc 269 | end 270 | end 271 | end 272 | end 273 | 274 | it 'should set the data and do a nodejs-style callback' do 275 | em do 276 | @zkem.connect do 277 | @zkem.set(@path, @new_data) do |exc,stat| 278 | exc.should be_nil 279 | stat.should be_instance_of(Zookeeper::Stat) 280 | EM.reactor_thread?.should be_true 281 | 282 | @zkem.get(@path) do |_,data| 283 | data.should == @new_data 284 | @zkem.close! { done } 285 | end 286 | end 287 | end 288 | end 289 | end 290 | end # success 291 | 292 | describe 'failure' do 293 | before do 294 | @path = [base_path, 'foo'].join('/') 295 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 296 | end 297 | 298 | it %[should call the errback in deferred style] do 299 | em do 300 | @zkem.connect do 301 | d = @zkem.set(@path, '') 302 | 303 | d.callback do 304 | raise "Should not have been called" 305 | end 306 | 307 | d.errback do |exc| 308 | exc.should be_kind_of(ZK::Exceptions::NoNode) 309 | @zkem.close! { done } 310 | end 311 | end 312 | end 313 | end 314 | 315 | it %[should have NoNode as the first argument to the block] do 316 | em do 317 | @zkem.connect do 318 | @zkem.set(@path, '') do |exc,_| 319 | exc.should be_kind_of(ZK::Exceptions::NoNode) 320 | @zkem.close! { done } 321 | end 322 | end 323 | end 324 | end 325 | end # failure 326 | end # set 327 | 328 | describe 'exists?' do 329 | before do 330 | @path = [base_path, 'foo'].join('/') 331 | @data = 'this is data' 332 | end 333 | 334 | it 'should call the block with true if the node exists' do 335 | @zk.create(@path, @data) 336 | 337 | em do 338 | @zkem.connect do 339 | dfr = @zkem.exists?(@path) 340 | 341 | dfr.callback do |bool| 342 | bool.should be_true 343 | @zkem.close! { done } 344 | end 345 | 346 | dfr.errback do |exc| 347 | raise exc 348 | end 349 | end 350 | end 351 | end 352 | 353 | it 'should call the block with false if the node does not exist' do 354 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 355 | 356 | em do 357 | @zkem.connect do 358 | dfr = @zkem.exists?(@path) 359 | 360 | dfr.callback do |bool| 361 | bool.should be_false 362 | @zkem.close! { done } 363 | end 364 | 365 | dfr.errback do |exc| 366 | raise exc 367 | end 368 | end 369 | end 370 | end 371 | end 372 | 373 | describe 'stat' do 374 | describe 'success' do 375 | before do 376 | @path = [base_path, 'foo'].join('/') 377 | @data = 'this is data' 378 | @zk.create(@path, @data) 379 | @orig_stat = @zk.stat(@path) 380 | end 381 | 382 | it 'should get the stat and call the callback' do 383 | em do 384 | @zkem.connect do 385 | dfr = @zkem.stat(@path) 386 | 387 | dfr.callback do |stat| 388 | stat.should_not be_nil 389 | stat.should == @orig_stat 390 | stat.should be_instance_of(Zookeeper::Stat) 391 | EM.reactor_thread?.should be_true 392 | @zkem.close! { done } 393 | end 394 | 395 | dfr.errback do |exc| 396 | raise exc 397 | end 398 | end 399 | end 400 | end 401 | 402 | it 'should get the stat and do a nodejs-style callback' do 403 | em do 404 | @zkem.connect do 405 | @zkem.stat(@path) do |exc,stat| 406 | exc.should be_nil 407 | stat.should be_instance_of(Zookeeper::Stat) 408 | EM.reactor_thread?.should be_true 409 | @zkem.close! { done } 410 | end 411 | end 412 | end 413 | end 414 | end # success 415 | 416 | describe 'non-existent node' do 417 | before do 418 | @path = [base_path, 'foo'].join('/') 419 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 420 | end 421 | 422 | it %[should not be an error to do stat on a non-existent node] do 423 | em do 424 | @zkem.connect do 425 | dfr = @zkem.stat(@path) 426 | 427 | dfr.callback do |stat| 428 | stat.should_not be_nil 429 | stat.exists?.should be_false 430 | stat.should be_instance_of(Zookeeper::Stat) 431 | EM.reactor_thread?.should be_true 432 | @zkem.close! { done } 433 | end 434 | 435 | dfr.errback do |exc| 436 | raise exc 437 | end 438 | end 439 | end 440 | end 441 | end # non-existent node 442 | end # stat 443 | 444 | describe 'delete' do 445 | describe 'success' do 446 | before do 447 | @path = [base_path, 'foo'].join('/') 448 | @data = 'this is data' 449 | @zk.create(@path, @data) 450 | end 451 | 452 | it 'should delete the node and call the callback' do 453 | em do 454 | @zkem.connect do 455 | d = @zkem.delete(@path) 456 | 457 | d.callback do |*a| 458 | a.should be_empty 459 | EM.reactor_thread?.should be_true 460 | @zkem.close! { done } 461 | end 462 | 463 | d.errback do |exc| 464 | raise exc 465 | end 466 | end 467 | end 468 | end 469 | 470 | it 'should delete the znode and do a nodejs-style callback' do 471 | em do 472 | @zkem.connect do 473 | @zkem.delete(@path) do |exc| 474 | exc.should be_nil 475 | EM.reactor_thread?.should be_true 476 | @zkem.close! { done } 477 | end 478 | end 479 | end 480 | end 481 | end # success 482 | 483 | describe 'failure' do 484 | before do 485 | @path = [base_path, 'foo'].join('/') 486 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 487 | end 488 | 489 | it %[should call the errback in deferred style] do 490 | em do 491 | @zkem.connect do 492 | d = @zkem.delete(@path) 493 | 494 | d.callback do 495 | raise "Should not have been called" 496 | end 497 | 498 | d.errback do |exc| 499 | exc.should be_kind_of(ZK::Exceptions::NoNode) 500 | @zkem.close! { done } 501 | end 502 | end 503 | end 504 | end 505 | 506 | it %[should have NoNode as the first argument to the block] do 507 | em do 508 | @zkem.connect do 509 | @zkem.delete(@path) do |exc,_| 510 | exc.should be_kind_of(ZK::Exceptions::NoNode) 511 | @zkem.close! { done } 512 | end 513 | end 514 | end 515 | end 516 | end # failure 517 | end # delete 518 | 519 | describe 'children' do 520 | describe 'success' do 521 | before do 522 | @path = [base_path, 'foo'].join('/') 523 | @child_1_path = [@path, 'child_1'].join('/') 524 | @child_2_path = [@path, 'child_2'].join('/') 525 | 526 | @data = 'this is data' 527 | @zk.create(@path, @data) 528 | @zk.create(@child_1_path, '') 529 | @zk.create(@child_2_path, '') 530 | end 531 | 532 | it 'should get the children and call the callback' do 533 | em do 534 | @zkem.connect do 535 | d = @zkem.children(@path) 536 | 537 | d.callback do |children,stat| 538 | children.should be_kind_of(Array) 539 | children.length.should == 2 540 | children.should include('child_1') 541 | children.should include('child_2') 542 | 543 | stat.should be_instance_of(Zookeeper::Stat) 544 | 545 | EM.reactor_thread?.should be_true 546 | @zkem.close! { done } 547 | end 548 | 549 | d.errback do |exc| 550 | raise exc 551 | end 552 | end 553 | end 554 | end 555 | 556 | it 'should get the children and do a nodejs-style callback' do 557 | em do 558 | @zkem.connect do 559 | @zkem.children(@path) do |exc, children, stat| 560 | exc.should be_nil 561 | children.should be_kind_of(Array) 562 | children.length.should == 2 563 | children.should include('child_1') 564 | children.should include('child_2') 565 | stat.should be_instance_of(Zookeeper::Stat) 566 | EM.reactor_thread?.should be_true 567 | @zkem.close! { done } 568 | end 569 | end 570 | end 571 | end 572 | end # success 573 | 574 | describe 'failure' do 575 | before do 576 | @path = [base_path, 'foo'].join('/') 577 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 578 | end 579 | 580 | it %[should call the errback in deferred style] do 581 | em do 582 | @zkem.connect do 583 | d = @zkem.children(@path) 584 | 585 | d.callback do 586 | raise "Should not have been called" 587 | end 588 | 589 | d.errback do |exc| 590 | exc.should be_kind_of(ZK::Exceptions::NoNode) 591 | @zkem.close! { done } 592 | end 593 | end 594 | end 595 | end 596 | 597 | it %[should have NoNode as the first argument to the block] do 598 | em do 599 | @zkem.connect do 600 | @zkem.children(@path) do |exc,_| 601 | exc.should be_kind_of(ZK::Exceptions::NoNode) 602 | @zkem.close! { done } 603 | end 604 | end 605 | end 606 | end 607 | end # failure 608 | end # children 609 | 610 | describe 'get_acl' do 611 | describe 'success' do 612 | before do 613 | @path = [base_path, 'foo'].join('/') 614 | @data = 'this is data' 615 | @zk.create(@path, @data) 616 | end 617 | 618 | it 'should get the data and call the callback' do 619 | em do 620 | @zkem.connect do 621 | dfr = @zkem.get_acl(@path) 622 | 623 | dfr.callback do |acls,stat| 624 | acls.should be_kind_of(Array) 625 | acls.first.should be_kind_of(Zookeeper::ACLs::ACL) 626 | stat.should be_instance_of(Zookeeper::Stat) 627 | 628 | EM.reactor_thread?.should be_true 629 | @zkem.close! { done } 630 | end 631 | 632 | dfr.errback do |exc| 633 | raise exc 634 | end 635 | end 636 | end 637 | end 638 | 639 | it 'should get the data and do a nodejs-style callback' do 640 | em do 641 | @zkem.connect do 642 | @zkem.get_acl(@path) do |exc,acls,stat| 643 | exc.should be_nil 644 | acls.should be_kind_of(Array) 645 | acls.first.should be_kind_of(Zookeeper::ACLs::ACL) 646 | stat.should be_instance_of(Zookeeper::Stat) 647 | EM.reactor_thread?.should be_true 648 | @zkem.close! { done } 649 | end 650 | end 651 | end 652 | end 653 | end # success 654 | 655 | describe 'failure' do 656 | before do 657 | @path = [base_path, 'foo'].join('/') 658 | @zk.delete(@path) rescue ZK::Exceptions::NoNode 659 | end 660 | 661 | it %[should call the errback in deferred style] do 662 | em do 663 | @zkem.connect do 664 | d = @zkem.get_acl(@path) 665 | 666 | d.callback do 667 | raise "Should not have been called" 668 | end 669 | 670 | d.errback do |exc| 671 | exc.should be_kind_of(ZK::Exceptions::NoNode) 672 | @zkem.close! { done } 673 | end 674 | end 675 | end 676 | end 677 | 678 | it %[should have NoNode as the first argument to the block] do 679 | em do 680 | @zkem.connect do 681 | @zkem.get_acl(@path) do |exc,*a| 682 | exc.should be_kind_of(ZK::Exceptions::NoNode) 683 | @zkem.close! { done } 684 | end 685 | end 686 | end 687 | end 688 | end # failure 689 | end # get_acl 690 | 691 | describe 'set_acl' do 692 | describe 'success' do 693 | it 'should set the acl and call the callback' 694 | it 'should set the acl and do a nodejs-style callback' 695 | end # success 696 | 697 | describe 'failure' do 698 | it %[should call the errback in deferred style] 699 | it %[should have NoNode as the first argument to the block] 700 | end # failure 701 | end # set_acl 702 | 703 | describe 'session_id and session_passwd' do 704 | it %[should return a Fixnum for session_id once connected] do 705 | em do 706 | @zkem.session_id.should be_nil 707 | 708 | @zkem.connect do 709 | @zkem.session_id.should be_kind_of(Fixnum) 710 | @zkem.close! { done } 711 | end 712 | end 713 | end 714 | 715 | it %[should return a String for session_passwd once connected] do 716 | em do 717 | @zkem.session_passwd.should be_nil 718 | 719 | @zkem.connect do 720 | @zkem.session_passwd.should be_kind_of(String) 721 | @zkem.close! { done } 722 | end 723 | end 724 | end 725 | end 726 | 727 | describe 'on_connection_lost' do 728 | before do 729 | @path = [base_path, 'foo'].join('/') 730 | @data = 'this is data' 731 | @zk.create(@path, @data) 732 | end 733 | 734 | it %[should be called back if the connection is lost] do 735 | em do 736 | @zkem.on_connection_lost do |exc| 737 | logger.debug { "WIN!" } 738 | exc.should be_kind_of(ZK::Exceptions::ConnectionLoss) 739 | @zkem.close! { done } 740 | end 741 | 742 | @zkem.connect do 743 | flexmock(@zkem.__send__(:cnx)) do |m| # ok, this is being a bit naughty with the __send__ 744 | m.should_receive(:get).with(Hash).and_return do |hash| 745 | logger.debug { "client received :get wtih #{hash.inspect}" } 746 | @user_cb = hash[:callback] 747 | 748 | EM.next_tick do 749 | logger.debug { "calling back user cb with connection loss" } 750 | @user_cb.call(:rc => ZK::Exceptions::CONNECTIONLOSS) 751 | end 752 | 753 | { :rc => Zookeeper::ZOK } 754 | end 755 | end 756 | 757 | @zkem.get(@path) do |exc,data| 758 | exc.should be_kind_of(ZK::Exceptions::ConnectionLoss) 759 | end 760 | end 761 | end 762 | end 763 | 764 | it %[should be called if we get a session expired event] do 765 | @zkem.on_connection_lost do |exc| 766 | logger.debug { "WIN!" } 767 | exc.should be_kind_of(ZK::Exceptions::ConnectionLoss) 768 | @zkem.close! { done } 769 | end 770 | 771 | em do 772 | @zkem.connect do 773 | event = event_mock(:connection_loss_event).tap do |ev| 774 | ev.should_receive(:state).and_return(Zookeeper::ZOO_EXPIRED_SESSION_STATE) 775 | end 776 | 777 | EM.next_tick { @zkem.event_handler.process(event) } 778 | end 779 | end 780 | end 781 | end # on_connection_lost 782 | 783 | describe 'on_connected' do 784 | it %[should be called back when a ZOO_CONNECTED_STATE event is received] do 785 | em do 786 | @zkem.on_connected do |event| 787 | logger.debug { "WIN!" } 788 | @zkem.close! { done } 789 | end 790 | 791 | @zkem.connect do 792 | logger.debug { "we connected" } 793 | end 794 | end 795 | end 796 | end # on_connected 797 | 798 | describe 'on_connecting' do 799 | it %[should be called back when a ZOO_CONNECTING_STATE event is received] do 800 | @zkem.on_connecting do |event| 801 | logger.debug { "WIN!" } 802 | @zkem.close! { done } 803 | end 804 | 805 | em do 806 | @zkem.connect do 807 | event = event_mock(:connecting_event).tap do |ev| 808 | ev.should_receive(:state).and_return(Zookeeper::ZOO_CONNECTING_STATE) 809 | end 810 | 811 | EM.next_tick { @zkem.event_handler.process(event) } 812 | end 813 | end 814 | end 815 | 816 | it %[should re-register when the hook in the documentation is used] do 817 | 818 | @perform_count = 0 819 | 820 | hook = proc do |ev| 821 | @zkem.on_connecting(&hook) 822 | 823 | if ev 824 | @perform_count += 1 825 | end 826 | 827 | if @perform_count == 2 828 | @zkem.close! { done } 829 | end 830 | end 831 | 832 | em do 833 | hook.call(nil) 834 | 835 | @zkem.connect do 836 | event = event_mock(:connecting_event).tap do |ev| 837 | ev.should_receive(:state).and_return(Zookeeper::ZOO_CONNECTING_STATE) 838 | end 839 | 840 | 2.times { EM.next_tick { @zkem.event_handler.process(event) } } 841 | end 842 | end 843 | end 844 | end # on_connecting 845 | end # Client 846 | 847 | describe 'regular' do 848 | 849 | before do 850 | @zkem = ZK::ZKEventMachine::Client.new('localhost:2181') 851 | @zk = ZK.new.tap { |z| wait_until { z.connected? } } 852 | @zk.should be_connected 853 | end 854 | 855 | it_should_behave_like 'Client' 856 | end 857 | 858 | describe 'chrooted' do 859 | let(:chroot_path) { '/_zkem_chroot_' } 860 | let(:zk_connect_host) { "localhost:2181#{chroot_path}" } 861 | 862 | before :all do 863 | ZK.open('localhost:2181') do |z| 864 | z.rm_rf(chroot_path) 865 | z.mkdir_p(chroot_path) 866 | end 867 | end 868 | 869 | after :all do 870 | ZK.open('localhost:2181') do |z| 871 | z.rm_rf(chroot_path) 872 | end 873 | end 874 | 875 | before do 876 | @zkem = ZK::ZKEventMachine::Client.new(zk_connect_host) 877 | @zk = ZK.new(zk_connect_host).tap { |z| wait_until { z.connected? } } 878 | @zk.should be_connected 879 | end 880 | 881 | it_should_behave_like 'Client' 882 | end 883 | end # ZK::ZKEventMachine 884 | 885 | --------------------------------------------------------------------------------