zk 0.8.9 → 0.9.0

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/pool.rb CHANGED
@@ -1,5 +1,22 @@
1
1
  module ZK
2
2
  module Pool
3
+ # Base class for a ZK connection pool. There are some applications that may
4
+ # require high synchronous throughput, which would be a suitable use for a
5
+ # connection pool. The ZK::Client::Threaded class is threadsafe, so it's
6
+ # not a problem accessing it from multiple threads, but it is limited to
7
+ # one outgoing synchronous request at a time, which could cause throughput
8
+ # issues for apps that are making very heavy use of zookeeper.
9
+ #
10
+ # The problem with using a connection pool is the added complexity when you
11
+ # try to use watchers. It may be possible to register a watch with one
12
+ # connection, and then call `:watch => true` on a different connection if
13
+ # you're not careful. Events delivered as part of an event handler have a
14
+ # `zk` attribute which can be used to access the connection that the
15
+ # callback is registered with.
16
+ #
17
+ # Unless you're sure you *need* a connection pool, then avoid the added
18
+ # complexity.
19
+ #
3
20
  class Base
4
21
  include Logging
5
22
 
@@ -40,7 +57,6 @@ module ZK
40
57
  end
41
58
 
42
59
  # close all the connections on the pool
43
- # @param optional Boolean graceful allow the checked out connections to come back first?
44
60
  def close_all!
45
61
  @mutex.synchronize do
46
62
  return unless open?
@@ -54,7 +70,8 @@ module ZK
54
70
 
55
71
  # calls close! on all connection objects, whether or not they're back in the pool
56
72
  # this is DANGEROUS!
57
- def force_close! #:nodoc:
73
+ # @private
74
+ def force_close!
58
75
  @mutex.synchronize do
59
76
  return if (closed? or forced?)
60
77
  @state = :forced
@@ -273,17 +290,18 @@ module ZK
273
290
  end
274
291
 
275
292
  def create_connection
276
- ZK.new(@host, @connection_timeout, @connection_args)
293
+ ZK.new(@host, @connection_args.merge(:timeout => @connection_timeout))
277
294
  end
278
295
  end # Bounded
279
296
 
280
297
  # create a connection pool useful for multithreaded applications
281
298
  #
282
- # Will spin up +number_of_connections+ at creation time and remain fixed at
299
+ # Will spin up `number_of_connections` at creation time and remain fixed at
283
300
  # that number for the life of the pool.
284
301
  #
285
- # ==== Example
286
- # pool = ZK::Pool::Simple.new("localhost:2181", 10)
302
+ # @example
303
+ #
304
+ # pool = ZK::Pool::Simple.new("localhost:2181", :timeout => 10)
287
305
  # pool.checkout do |zk|
288
306
  # zk.create("/mynew_path")
289
307
  # end
data/lib/z_k/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ZK
2
- VERSION = "0.8.9"
2
+ VERSION = "0.9.0"
3
3
  end
data/lib/z_k.rb CHANGED
@@ -25,8 +25,7 @@ require 'z_k/find'
25
25
  module ZK
26
26
  ZK_ROOT = File.expand_path('../..', __FILE__)
27
27
 
28
- KILL_TOKEN = :__ZK_kILL_tOkEn__ #:nodoc:
29
-
28
+ KILL_TOKEN = Object.new unless defined?(KILL_TOKEN)
30
29
 
31
30
  # The logger used by the ZK library. uses a Logger to +/dev/null+ by default
32
31
  #
@@ -0,0 +1,15 @@
1
+ shared_context 'threaded client connection' do
2
+ before do
3
+ @connection_string = "localhost:#{ZK_TEST_PORT}"
4
+ @zk = ZK::Client::Threaded.new(@connection_string).tap { |z| wait_until { z.connected? } }
5
+ @zk.rm_rf('/test')
6
+ end
7
+
8
+ after do
9
+ @zk.rm_rf('/test')
10
+ @zk.close!
11
+
12
+ wait_until(2) { @zk.closed? }
13
+ end
14
+ end
15
+
@@ -0,0 +1,155 @@
1
+ shared_examples_for 'client' do
2
+ describe :mkdir_p do
3
+ before(:each) do
4
+ @path_ary = %w[test mkdir_p path creation]
5
+ @bogus_path = File.join('/', *@path_ary)
6
+ end
7
+
8
+ it %[should create all intermediate paths for the path givem] do
9
+ @zk.should_not be_exists(@bogus_path)
10
+ @zk.should_not be_exists(File.dirname(@bogus_path))
11
+ @zk.mkdir_p(@bogus_path)
12
+ @zk.should be_exists(@bogus_path)
13
+ end
14
+ end
15
+
16
+ describe :stat do
17
+ describe 'for a missing node' do
18
+ before do
19
+ @missing_path = '/thispathdoesnotexist'
20
+ @zk.delete(@missing_path) rescue ZK::Exceptions::NoNode
21
+ end
22
+
23
+ it %[should not raise any error] do
24
+ lambda { @zk.stat(@missing_path) }.should_not raise_error
25
+ end
26
+
27
+ it %[should return a Stat object] do
28
+ @zk.stat(@missing_path).should be_kind_of(ZookeeperStat::Stat)
29
+ end
30
+
31
+ it %[should return a stat that not exists?] do
32
+ @zk.stat(@missing_path).should_not be_exists
33
+ end
34
+ end
35
+ end
36
+
37
+ describe :block_until_node_deleted do
38
+ before do
39
+ @path = '/_bogualkjdhsna'
40
+ end
41
+
42
+ describe 'no node initially' do
43
+ before do
44
+ @zk.exists?(@path).should be_false
45
+ end
46
+
47
+ it %[should not block] do
48
+ @a = false
49
+
50
+ th = Thread.new do
51
+ @zk.block_until_node_deleted(@path)
52
+ @a = true
53
+ end
54
+
55
+ th.join(2)
56
+ @a.should be_true
57
+ end
58
+ end
59
+
60
+ describe 'node exists initially' do
61
+ before do
62
+ @zk.create(@path, '', :mode => :ephemeral)
63
+ @zk.exists?(@path).should be_true
64
+ end
65
+
66
+ it %[should block until the node is deleted] do
67
+ @a = false
68
+
69
+ th = Thread.new do
70
+ @zk.block_until_node_deleted(@path)
71
+ @a = true
72
+ end
73
+
74
+ Thread.pass
75
+ @a.should be_false
76
+
77
+ @zk.delete(@path)
78
+
79
+ wait_until(2) { @a }
80
+ @a.should be_true
81
+ end
82
+ end
83
+ end
84
+
85
+ describe 'session_id and session_passwd' do
86
+ it %[should expose the underlying session_id] do
87
+ @zk.session_id.should be_kind_of(Fixnum)
88
+ end
89
+
90
+ it %[should expose the underlying session_passwd] do
91
+ @zk.session_passwd.should be_kind_of(String)
92
+ end
93
+ end
94
+
95
+ describe 'reopen' do
96
+ describe 'watchers' do
97
+ before do
98
+ @path = '/testwatchers'
99
+ @queue = Queue.new
100
+ @zk.delete(@path) rescue ZK::Exceptions::NoNode
101
+ end
102
+
103
+ # after do
104
+ # logger.info { "AFTER EACH" }
105
+ # @zk.delete(@path)
106
+ # end
107
+
108
+ def ensure_event_delivery!
109
+ @sub ||= @zk.event_handler.register(@path) do |event|
110
+ logger.debug { "got event: #{event.inspect}" }
111
+ @queue << event
112
+ end
113
+
114
+ @zk.exists?(@path, :watch => true).should be_false
115
+ @zk.create(@path, '')
116
+
117
+ logger.debug { "waiting for event delivery" }
118
+
119
+ wait_until(2) do
120
+ begin
121
+ @events << @queue.pop(true)
122
+ true
123
+ rescue ThreadError
124
+ false
125
+ end
126
+ end
127
+
128
+ # first watch delivered correctly
129
+ @events.length.should > 0
130
+ end
131
+
132
+ it %[should fire re-registered watchers after reopen (#9)] do
133
+ @events = []
134
+
135
+ logger.debug { "ensure event delivery" }
136
+ ensure_event_delivery!
137
+
138
+ logger.debug { "reopening connection" }
139
+ @zk.reopen
140
+
141
+ wait_until(2) { @zk.connected? }
142
+
143
+ logger.debug { "deleting path" }
144
+ @zk.delete(@path)
145
+
146
+ logger.debug { "clearing events" }
147
+ @events.clear
148
+
149
+ logger.debug { "taunt them a second time" }
150
+ ensure_event_delivery!
151
+ end
152
+ end
153
+ end
154
+ end
155
+
data/spec/spec_helper.rb CHANGED
@@ -8,65 +8,25 @@ require 'benchmark'
8
8
 
9
9
  ZK_TEST_PORT = 2181
10
10
 
11
- LOG_FILE = File.open(File.join(ZK::ZK_ROOT, 'test.log'), 'a').tap { |f| f.sync = true }
12
-
13
- ZK.logger = Logger.new(LOG_FILE).tap { |log| log.level = Logger::DEBUG }
14
- Zookeeper.logger = ZK.logger
15
-
16
- ZK.logger.debug { "LOG OPEN" }
17
-
18
11
  # Requires supporting ruby files with custom matchers and macros, etc,
19
12
  # in spec/support/ and its subdirectories.
20
- Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}
13
+ Dir[File.expand_path("../{support,shared}/**/*.rb", __FILE__)].each {|f| require f}
21
14
 
22
15
  $stderr.sync = true
23
16
 
24
- # COMMENT THESE LINES FOR REMOTE DEBUGGING
25
- # require 'ruby-debug'
26
17
  require 'flexmock'
27
18
 
28
19
  RSpec.configure do |config|
29
20
  config.mock_with :flexmock
30
21
  config.include(FlexMock::ArgumentTypes)
31
22
 
32
- # config.before(:all) do
33
- # unless $did_debug
34
- # $did_debug = true
35
- # $stderr.puts "debugger started? is #{Debugger.started?.inspect}"
23
+ config.include(WaitWatchers)
24
+ config.extend(WaitWatchers)
36
25
 
37
- # Debugger.wait_connection = true
38
- # $stderr.puts "run 'rdebug -c -p #{Debugger::PORT}'"
39
- # Debugger.start_remote
40
-
41
- # config.debug = true
42
- # end
43
- # end
26
+ config.include(SpecGlobalLogger)
27
+ config.extend(SpecGlobalLogger)
44
28
  end
45
29
 
46
- def logger
47
- ZK.logger
48
- end
49
-
50
- # method to wait until block passed returns true or timeout (default is 2 seconds) is reached
51
- def wait_until(timeout=2)
52
- time_to_stop = Time.now + timeout
53
-
54
- until yield
55
- break if Time.now > time_to_stop
56
- Thread.pass
57
- end
58
- end
59
-
60
- def wait_while(timeout=2)
61
- time_to_stop = Time.now + timeout
62
-
63
- while yield
64
- break if Time.now > time_to_stop
65
- Thread.pass
66
- end
67
- end
68
-
69
-
70
30
  class ::Thread
71
31
  # join with thread until given block is true, the thread joins successfully,
72
32
  # or timeout seconds have passed
@@ -76,7 +36,8 @@ class ::Thread
76
36
 
77
37
  until yield
78
38
  break if Time.now > time_to_stop
79
- break if join(0.1)
39
+ break if join(0)
40
+ Thread.pass
80
41
  end
81
42
  end
82
43
 
@@ -85,15 +46,10 @@ class ::Thread
85
46
 
86
47
  while yield
87
48
  break if Time.now > time_to_stop
88
- break if join(0.1)
49
+ break if join(0)
50
+ Thread.pass
89
51
  end
90
52
  end
91
53
  end
92
54
 
93
- def report_realtime(what)
94
- return yield
95
- t = Benchmark.realtime { yield }
96
- $stderr.puts "#{what}: %0.3f" % [t.to_f]
97
- end
98
-
99
55
 
@@ -0,0 +1,27 @@
1
+ module ZK
2
+ LOG_FILE = File.open(File.join(ZK::ZK_ROOT, 'test.log'), 'a').tap { |f| f.sync = true }
3
+ # LOG_FILE = $stderr
4
+ end
5
+
6
+ ZK.logger = Logger.new(ZK::LOG_FILE).tap { |log| log.level = Logger::DEBUG }
7
+
8
+ # Zookeeper.logger = ZK.logger
9
+ # Zookeeper.set_debug_level(4)
10
+
11
+ ZK.logger.debug { "LOG OPEN" }
12
+
13
+ module SpecGlobalLogger
14
+ def logger
15
+ ZK.logger
16
+ end
17
+
18
+ # sets the log level to FATAL for the duration of the block
19
+ def mute_logger
20
+ orig_level, ZK.logger.level = ZK.logger.level, Logger::FATAL
21
+ orig_zk_level, Zookeeper.debug_level = Zookeeper.debug_level, ZookeeperConstants::ZOO_LOG_LEVEL_ERROR
22
+ yield
23
+ ensure
24
+ ZK.logger.level = orig_level
25
+ end
26
+ end
27
+
@@ -0,0 +1,6 @@
1
+ module ZK
2
+ # nothing to see here, move along
3
+ class SpecialHappyFuntimeError < RuntimeError
4
+ end
5
+ end
6
+
@@ -0,0 +1,48 @@
1
+ module WaitWatchers
2
+ class TimeoutError < StandardError; end
3
+
4
+ # method to wait until block passed returns truthy (false will not work) or
5
+ # timeout (default is 2 seconds) is reached raises TiemoutError on timeout
6
+ #
7
+ # @returns the truthy value
8
+ #
9
+ # @example
10
+ #
11
+ # @a = nil
12
+ #
13
+ # th = Thread.new do
14
+ # sleep(1)
15
+ # @a = :fudge
16
+ # end
17
+ #
18
+ # wait_until(2) { @a }.should == :fudge
19
+ #
20
+ def wait_until(timeout=2)
21
+ time_to_stop = Time.now + timeout
22
+ while true
23
+ rval = yield
24
+ return rval if rval
25
+ raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop
26
+ Thread.pass
27
+ end
28
+ end
29
+
30
+ # inverse of wait_until
31
+ def wait_while(timeout=2)
32
+ time_to_stop = Time.now + timeout
33
+ while true
34
+ rval = yield
35
+ return rval unless rval
36
+ raise TimeoutError, "timeout of #{timeout}s exceeded" if Time.now > time_to_stop
37
+ Thread.pass
38
+ end
39
+ end
40
+
41
+ def report_realtime(what)
42
+ return yield
43
+ t = Benchmark.realtime { yield }
44
+ $stderr.puts "#{what}: %0.3f" % [t.to_f]
45
+ end
46
+ end
47
+
48
+
data/spec/watch_spec.rb CHANGED
@@ -16,7 +16,9 @@ describe ZK do
16
16
  wait_until { !@zk.connected? }
17
17
  end
18
18
 
19
- ZK.open(@cnx_str) { |zk| zk.rm_rf(@path) }
19
+ mute_logger do
20
+ ZK.open(@cnx_str) { |zk| zk.rm_rf(@path) }
21
+ end
20
22
  end
21
23
 
22
24
  it "should call back to path registers" do
@@ -37,6 +39,16 @@ describe ZK do
37
39
  callback_called.should be_true
38
40
  end
39
41
 
42
+ # this is stupid, and a bad test, but we have to check that events
43
+ # don't get re-delivered to a single registered callback just because
44
+ # :watch => true was called twice
45
+ #
46
+ # again, we're testing a negative here, so consider this a regression check
47
+ #
48
+ def wait_for_events_to_not_be_delivered(events)
49
+ lambda { wait_until(0.2) { events.length >= 2 } }.should raise_error(WaitWatchers::TimeoutError)
50
+ end
51
+
40
52
  it %[should only deliver an event once to each watcher registered for exists?] do
41
53
  events = []
42
54
 
@@ -51,7 +63,8 @@ describe ZK do
51
63
 
52
64
  @zk.create(@path, '', :mode => :ephemeral)
53
65
 
54
- wait_until { events.length >= 2 }
66
+ wait_for_events_to_not_be_delivered(events)
67
+
55
68
  events.length.should == 1
56
69
  end
57
70
 
@@ -72,7 +85,8 @@ describe ZK do
72
85
 
73
86
  @zk.set(@path, 'two')
74
87
 
75
- wait_until { events.length >= 2 }
88
+ wait_for_events_to_not_be_delivered(events)
89
+
76
90
  events.length.should == 1
77
91
  end
78
92
 
@@ -94,7 +108,8 @@ describe ZK do
94
108
 
95
109
  @zk.create("#{@path}/pfx", '', :mode => :ephemeral_sequential)
96
110
 
97
- wait_until { events.length >= 2 }
111
+ wait_for_events_to_not_be_delivered(events)
112
+
98
113
  events.length.should == 1
99
114
  end
100
115
  end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ module ZK
4
+ module Client
5
+ describe 'ZK::Client::DropBox' do
6
+ let(:drop_box) { DropBox.new }
7
+
8
+ after do
9
+ DropBox.remove_current
10
+ end
11
+
12
+ it %[should start out with an undefined value] do
13
+ drop_box.value.should == DropBox::UNDEFINED
14
+ end
15
+
16
+ it %[should block the caller waiting for a response] do
17
+ @rv = nil
18
+
19
+ th1 = Thread.new do
20
+ Thread.current.abort_on_exception = true
21
+ @rv = drop_box.pop
22
+ end
23
+
24
+ wait_until(2) { th1.status == 'sleep' }
25
+
26
+ th1.status.should == 'sleep'
27
+
28
+ th2 = Thread.new do
29
+ drop_box.push :result
30
+ end
31
+
32
+ th2.join(2).should == th2
33
+ th1.join(2).should == th1
34
+
35
+ @rv.should == :result
36
+ end
37
+
38
+ describe :oh_noes! do
39
+ let(:error_class) { SpecialHappyFuntimeError }
40
+ let(:error_msg) { 'this is a unique message' }
41
+
42
+ it %[should wake the caller by raising the exception class and message given] do
43
+ drop_box.should_not be_done # sanity check
44
+
45
+ th1 = Thread.new do
46
+ drop_box.pop
47
+ end
48
+
49
+ wait_until(2) { th1.status == 'sleep' }
50
+
51
+ th1.status.should == 'sleep'
52
+
53
+ drop_box.oh_noes!(error_class, error_msg).should_not be_nil
54
+
55
+ lambda { th1.join(2) }.should raise_error(error_class, error_msg)
56
+
57
+ drop_box.should be_done
58
+ end
59
+ end
60
+
61
+ describe :done? do
62
+ it %[should be done if the value is defined] do
63
+ drop_box.should_not be_done
64
+ drop_box.push :defined
65
+ drop_box.should be_done
66
+ end
67
+
68
+ it %[should not be done once cleared] do
69
+ drop_box.push :defined
70
+ drop_box.should be_done
71
+ drop_box.clear
72
+ drop_box.should_not be_done
73
+ end
74
+ end
75
+
76
+ describe :with_current do
77
+ it %[should clear the current thread's drop_box once the block exits] do
78
+ DropBox.with_current do |c|
79
+ c.should_not be_done
80
+ c.push 'yo_mama'
81
+ c.should be_done
82
+ end
83
+
84
+ DropBox.current.should_not be_done
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'session death' do
4
+ def deliver_session_event_to(event_num, zk)
5
+ # jeez, Zookeeper callbacks are so frustratingly stupid
6
+ bogus_event = ZookeeperCallbacks::WatcherCallback.new
7
+ bogus_event.initialize_context(:type => -1, :state => event_num, :path => '', :context => 'bogustestevent')
8
+ # XXX: this is bad because we're in the wrong thread, but we'll fix this after the next Zookeeper release
9
+ zk.event_handler.process(bogus_event)
10
+ end
11
+
12
+ before do
13
+ @other_zk = ZK.new(@connection_string)
14
+ end
15
+
16
+ after do
17
+ @other_zk.close! unless @other_zk.closed?
18
+ end
19
+
20
+ it %[should wake up in the case of an expired session and throw an exception] do
21
+ @a = false
22
+
23
+ @other_zk.event_handler.register_state_handler(zoo_state) do |event|
24
+ @a = event
25
+ end
26
+
27
+ th = Thread.new do
28
+ @other_zk.block_until_node_deleted(@path)
29
+ end
30
+
31
+ # we don't expect an exception yet, so warn us if there is on while this
32
+ # thread is on its way to sleep
33
+ th.abort_on_exception = true
34
+
35
+ wait_until(2) { th.status == 'sleep' }
36
+
37
+ th.abort_on_exception = false # after here, we're raising an exception on purpose
38
+
39
+ th.join if th.status.nil? # this indicates an exception happened...already
40
+
41
+ # not on the other thread, this may be bad
42
+ deliver_session_event_to(zoo_state, @other_zk)
43
+
44
+ # ditto, this is probably happening synchrnously
45
+ wait_until(2) { @a }.should be_true
46
+
47
+ lambda { th.join(2) }.should raise_error(zoo_error_class)
48
+ end
49
+ end # session death
50
+
51
+ shared_examples_for 'locking and session death' do
52
+ describe 'exceptional conditions' do
53
+ describe 'ZOO_EXPIRED_SESSION_STATE' do
54
+ let(:zoo_state) { ZookeeperConstants::ZOO_EXPIRED_SESSION_STATE }
55
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::SessionExpired }
56
+
57
+ it_behaves_like 'session death'
58
+ end
59
+
60
+ describe 'ZOO_CONNECTING_STATE' do
61
+ let(:zoo_state) { ZookeeperConstants::ZOO_CONNECTING_STATE }
62
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::NotConnected }
63
+
64
+ it_behaves_like 'session death'
65
+ end
66
+
67
+ describe 'ZOO_CLOSED_STATE' do
68
+ let(:zoo_state) { ZookeeperConstants::ZOO_CLOSED_STATE }
69
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::ConnectionClosed }
70
+
71
+ it_behaves_like 'session death'
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'threaded client and locking behavior' do
77
+ include_context 'threaded client connection'
78
+
79
+ before do
80
+ @path = '/test'
81
+ @zk.create('/test') rescue ZK::Exceptions::NodeExists
82
+ end
83
+
84
+ describe 'block_until_node_deleted' do
85
+ it %[should raise an EventDispatchThreadException if called in the context of the event dispatch thread] do
86
+ @exception = nil
87
+
88
+ @zk.register(@path) do |event|
89
+ @zk.event_dispatch_thread?.should be_true
90
+
91
+ begin
92
+ @zk.block_until_node_deleted(@path)
93
+ rescue Exception => e
94
+ @exception = e
95
+ end
96
+ end
97
+
98
+ @zk.exists?(@path, :watch => true)
99
+
100
+ @zk.set(@path, 'blah')
101
+
102
+ wait_until(2) { @exception }.should be_kind_of(ZK::Exceptions::EventDispatchThreadException)
103
+ end
104
+ end
105
+
106
+ it_should_behave_like 'locking and session death'
107
+ end
108
+