zk 0.8.9 → 0.9.0

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