zk 0.8.8 → 0.8.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ ---
2
+ notifications:
3
+ email:
4
+ - slyphon@gmail.com
5
+
6
+ rvm:
7
+ - 1.8.7
8
+ - 1.9.2
9
+ - 1.9.3
10
+ - jruby
11
+
12
+ before_install:
13
+ - sudo apt-get update
14
+ - sudo apt-get install zookeeperd
15
+
16
+ bundler_args: --without development
17
+
data/Gemfile CHANGED
@@ -2,6 +2,17 @@
2
2
  source ENV['MBOX_BUNDLER_SOURCE'] if ENV['MBOX_BUNDLER_SOURCE']
3
3
  source "http://rubygems.org"
4
4
 
5
+ group :development do
6
+ gem 'pry'
7
+ end
8
+
9
+ group :test do
10
+ gem 'rspec', '~> 2.8.0'
11
+ gem 'flexmock', '~> 0.8.10'
12
+ gem 'ZenTest', '~> 4.5.0'
13
+ gem 'rake'
14
+ end
15
+
5
16
  # Specify your gem's dependencies in zk.gemspec
6
17
  gemspec
7
18
 
@@ -1,8 +1,6 @@
1
1
  # ZK
2
2
 
3
- ZK is a high-level interface to the Apache [ZooKeeper][] server. It is based on the [zookeeper gem][] which is a multi-Ruby low-level driver. Currently MRI 1.8.7, 1.9.2, and JRuby are supported (rubinius 1.2 is experimental but _should_ work). It is licensed under the [MIT][] license.
4
-
5
- Note: 1.9.3-p0 support is currently under development, there are a few bugs to work out still...
3
+ ZK is a high-level interface to the Apache [ZooKeeper][] server. It is based on the [zookeeper gem][] which is a multi-Ruby low-level driver. Currently MRI 1.8.7, 1.9.2, 1.9.3, and JRuby are supported (rubinius 1.2 is experimental but _should_ work). It is licensed under the [MIT][] license.
6
4
 
7
5
  This library is heavily used in a production deployment and is actively developed and maintained.
8
6
 
@@ -35,13 +33,13 @@ The [zookeeper gem][] provides a low-level, cross platform library for interfaci
35
33
  ZK provides:
36
34
 
37
35
  * a robust lock implementation (both shared and exclusive locks)
38
- * an extension for the [Mongoid][] ORM to provide advisory locks on mongodb records
39
36
  * a leader election implementation with both "leader" and "observer" roles
40
37
  * a higher-level interface to the ZooKeeper callback/watcher mechanism than the [zookeeper gem][] provides
41
38
  * a simple threadpool implementation
42
39
  * a bounded, dynamically-growable (threadsafe) client pool implementation
43
40
  * a recursive Find class (like the Find module in ruby-core)
44
41
  * unix-like rm\_rf and mkdir\_p methods (useful for functional testing)
42
+ * an extension for the [Mongoid][] ORM to provide advisory locks on mongodb records
45
43
 
46
44
  In addition to all of that, I would like to think that the public API the ZK::Client provides is more convenient to use for the common (synchronous) case. For use with [EventMachine][] there is [zk-eventmachine][] which provides a convenient API for writing evented code that uses the ZooKeeper server.
47
45
 
@@ -58,7 +56,7 @@ ZK strives to be a complete, correct, and convenient way of interacting with Zoo
58
56
 
59
57
  * ZK::Client supports asynchronous calls of all basic methods (get, set, delete, etc.) however these versions are kind of inconvenient to use. For a fully evented stack, try [zk-eventmachine][], which is designed to be compatible and convenient to use in event-driven code.
60
58
 
61
- * ZooKeeper "chroot" [connection syntax][chroot] _(search for "chroot" in page)_ is not currently working in the C drivers, and I don't have tests for the Java version. This hasn't been an incredibly high priority item, but support for this feature is intended.
59
+ * ZooKeeper "chroot" [connection syntax][chroot] is currently being developed and should work for most cases. Right now we require that the root path exist before the chrooted client is used, but that may change [in the near future](https://github.com/slyphon/zk/issues/7).
62
60
 
63
61
  * I am currently in the process of cleaning up the API documentation and converting it to use [YARD][].
64
62
 
data/Rakefile CHANGED
@@ -31,3 +31,19 @@ gemset_name = 'zk'
31
31
  task "mb:test_all" => rspec_task_name
32
32
  end
33
33
 
34
+ namespace :spec do
35
+ task :define do
36
+ require 'rubygems'
37
+ require 'bundler/setup'
38
+ require 'rspec/core/rake_task'
39
+
40
+ RSpec::Core::RakeTask.new('spec:runner')
41
+ end
42
+
43
+ task :run => :define do
44
+ Rake::Task['spec:runner'].invoke
45
+ end
46
+ end
47
+
48
+ task :default => 'spec:run'
49
+
@@ -48,8 +48,6 @@ module ZK
48
48
  end
49
49
  end
50
50
 
51
- # @private
52
- #
53
51
  # Register a block to be called on connection, when the client has
54
52
  # connected.
55
53
  #
@@ -1,6 +1,8 @@
1
1
  module ZK
2
2
  module Client
3
3
  module Unixisms
4
+ include ZookeeperConstants
5
+
4
6
  # Creates all parent paths and 'path' in zookeeper as persistent nodes with
5
7
  # zero data.
6
8
  #
@@ -62,7 +64,7 @@ module ZK
62
64
  # watcher, you *will* deadlock!
63
65
  def block_until_node_deleted(abs_node_path)
64
66
  queue = Queue.new
65
- ev_sub = nil
67
+ subs = []
66
68
 
67
69
  node_deletion_cb = lambda do |event|
68
70
  if event.node_deleted?
@@ -72,16 +74,35 @@ module ZK
72
74
  end
73
75
  end
74
76
 
75
- ev_sub = watcher.register(abs_node_path, &node_deletion_cb)
77
+ subs << event_handler.register(abs_node_path, &node_deletion_cb)
78
+
79
+ # NOTE: this pattern may be necessary for other features with blocking semantics!
80
+
81
+ session_cb = lambda do |event|
82
+ queue.enq(event.state)
83
+ end
76
84
 
85
+ [:expired_session, :connecting, :closed].each do |sym|
86
+ subs << event_handler.register_state_handler(sym, &session_cb)
87
+ end
88
+
77
89
  # set up the callback, but bail if we don't need to wait
78
90
  return true unless exists?(abs_node_path, :watch => true)
79
91
 
80
- queue.pop # block waiting for node deletion
81
- true
92
+ case queue.pop
93
+ when :deleted
94
+ true
95
+ when ZOO_EXPIRED_SESSION_STATE
96
+ raise ZookeeperExceptions::ZookeeperException::SessionExpired
97
+ when ZOO_CONNECTING_STATE
98
+ raise ZookeeperExceptions::ZookeeperException::NotConnected
99
+ when ZOO_CLOSED_STATE
100
+ raise ZookeeperExceptions::ZookeeperException::ConnectionClosed
101
+ else
102
+ raise "Hit unexpected case in block_until_node_deleted"
103
+ end
82
104
  ensure
83
- # be sure we clean up after ourselves
84
- ev_sub.unregister if ev_sub
105
+ subs.each(&:unregister)
85
106
  end
86
107
  end
87
108
  end
@@ -85,9 +85,16 @@ module ZK
85
85
  end
86
86
  alias :subscribe :register
87
87
 
88
- # registers a "state of the connection" handler
88
+ # Registers a "state of the connection" handler
89
89
  #
90
- # @param [String] state the state you want to register for
90
+ # Valid states are: connecting, associating, connected, auth_failed,
91
+ # expired_session. Of all of these, you are probably most likely
92
+ # interested in `expired_session` and `connecting`, which are fired
93
+ # when you either lose your session (and have to completely reconnect),
94
+ # or when there's a temporary loss in connection and Zookeeper recommends
95
+ # you go into 'safe mode'.
96
+ #
97
+ # @param [String] state The state you want to register for.
91
98
  # @param [Block] block the block to execute on state changes
92
99
  # @yield [event] yields your block with
93
100
  #
@@ -71,6 +71,7 @@ module ZK
71
71
  def state_event?
72
72
  path.nil? or path.empty?
73
73
  end
74
+ alias session_event? state_event?
74
75
 
75
76
  # has this watcher been called because of a change to a zookeeper node?
76
77
  def node_event?
@@ -1,3 +1,3 @@
1
1
  module ZK
2
- VERSION = "0.8.8"
2
+ VERSION = "0.8.9"
3
3
  end
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'zk'
4
+ require 'logger'
5
+
6
+ $stderr.sync = true
7
+
8
+ ZK.logger = log = Logger.new('informal.log').tap { |l| l.level = Logger::DEBUG }
9
+ Zookeeper.logger = log
10
+ Zookeeper.set_debug_level(4)
11
+
12
+ class ::Exception
13
+ def to_std_format
14
+ "#{self.class}: #{message}\n" + backtrace {|n| "\t#{n}"}.join("\n")
15
+ end
16
+ end
17
+
18
+ def safe_join(th, timeout=nil)
19
+ begin
20
+ th.join(timeout)
21
+ rescue Exception => e
22
+ $stderr.puts "#{th[:name]} raised #{e.to_std_format}"
23
+ end
24
+ end
25
+
26
+ def print_error
27
+ yield
28
+ rescue Exception => e
29
+ $stderr.puts "caught exception in #{Thread.current[:name]}: #{e.to_std_format}"
30
+ end
31
+
32
+ lock_name = 'the_big_sleep'
33
+
34
+ q = Queue.new
35
+
36
+
37
+ th1 = Thread.new do
38
+ print_error do
39
+ ZK.open do |zk|
40
+ $stderr.puts "first connection session_id: 0x%x" % zk.session_id
41
+ sub = zk.on_expired_session do |state|
42
+ $stderr.puts "OH NOES! thread 1 got an expired session! #{state.inspect}"
43
+ end
44
+
45
+ zk.with_lock(lock_name) do
46
+ q.push(:ok_sleeping)
47
+ sleep # we now sleep with the fishes
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ th1[:name] = 'thread 1'
54
+
55
+ q.pop
56
+
57
+ Thread.pass until th1.status == 'sleep'
58
+
59
+ $stderr.puts "ok, now try to acquire lock"
60
+
61
+ th2 = Thread.new do
62
+ print_error do
63
+ ZK.open do |zk|
64
+ $stderr.puts "second connection session_id: 0x%x" % zk.session_id
65
+
66
+ sub = zk.on_expired_session do |state|
67
+ $stderr.puts "OH NOES! thread 2 got an expired session! #{state.inspect}"
68
+ end
69
+
70
+ zk.with_lock(lock_name) do
71
+ $stderr.puts "acquired the lock in second thread"
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ th2[:name] = 'thread 2'
78
+
79
+ [th1, th2].each(&method(:safe_join))
80
+
@@ -11,7 +11,9 @@ ZK_TEST_PORT = 2181
11
11
  LOG_FILE = File.open(File.join(ZK::ZK_ROOT, 'test.log'), 'a').tap { |f| f.sync = true }
12
12
 
13
13
  ZK.logger = Logger.new(LOG_FILE).tap { |log| log.level = Logger::DEBUG }
14
- #Zookeeper.logger = ZK.logger
14
+ Zookeeper.logger = ZK.logger
15
+
16
+ ZK.logger.debug { "LOG OPEN" }
15
17
 
16
18
  # Requires supporting ruby files with custom matchers and macros, etc,
17
19
  # in spec/support/ and its subdirectories.
@@ -2,7 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe ZK::Client do
4
4
  before do
5
- @zk = ZK.new("localhost:#{ZK_TEST_PORT}")
5
+ @connection_string = "localhost:#{ZK_TEST_PORT}"
6
+ @zk = ZK.new(@connection_string)
6
7
  wait_until{ @zk.connected? }
7
8
  @zk.rm_rf('/test')
8
9
  end
@@ -82,6 +83,7 @@ describe ZK::Client do
82
83
  @a = false
83
84
 
84
85
  th = Thread.new do
86
+
85
87
  @zk.block_until_node_deleted(@path)
86
88
  @a = true
87
89
  end
@@ -94,6 +96,69 @@ describe ZK::Client do
94
96
  wait_until(2) { @a }
95
97
  @a.should be_true
96
98
  end
99
+
100
+ shared_examples_for 'session death' do
101
+ def deliver_session_event_to(event_num, zk)
102
+ # jeez, Zookeeper callbacks are so frustratingly stupid
103
+ bogus_event = ZookeeperCallbacks::WatcherCallback.new
104
+ bogus_event.initialize_context(:type => -1, :state => event_num, :path => '', :context => 'bogustestevent')
105
+ # XXX: this is bad because we're in the wrong thread, but we'll fix this after the next Zookeeper release
106
+ zk.event_handler.process(bogus_event)
107
+ end
108
+
109
+ before do
110
+ @other_zk = ZK.new(@connection_string)
111
+ end
112
+
113
+ after do
114
+ @other_zk.close! unless @other_zk.closed?
115
+ end
116
+
117
+ it %[should wake up in the case of an expired session and throw an exception] do
118
+ @a = false
119
+
120
+ @other_zk.event_handler.register_state_handler(zoo_state) do |event|
121
+ @a = event
122
+ end
123
+
124
+ th = Thread.new do
125
+ @other_zk.block_until_node_deleted(@path)
126
+ end
127
+
128
+ wait_until(2) { th.status == 'sleep' }
129
+
130
+ # not on the other thread, this may be bad
131
+ deliver_session_event_to(zoo_state, @other_zk)
132
+
133
+ # ditto, this is probably happening synchrnously
134
+ wait_until(2) { @a }
135
+
136
+ lambda { th.join(2) }.should raise_error(zoo_error_class)
137
+ end
138
+ end
139
+
140
+ describe 'exceptional conditions' do
141
+ describe 'ZOO_EXPIRED_SESSION_STATE' do
142
+ let(:zoo_state) { ZookeeperConstants::ZOO_EXPIRED_SESSION_STATE }
143
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::SessionExpired }
144
+
145
+ it_behaves_like 'session death'
146
+ end
147
+
148
+ describe 'ZOO_CONNECTING_STATE' do
149
+ let(:zoo_state) { ZookeeperConstants::ZOO_CONNECTING_STATE }
150
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::NotConnected }
151
+
152
+ it_behaves_like 'session death'
153
+ end
154
+
155
+ describe 'ZOO_CLOSED_STATE' do
156
+ let(:zoo_state) { ZookeeperConstants::ZOO_CLOSED_STATE }
157
+ let(:zoo_error_class) { ZookeeperExceptions::ZookeeperException::ConnectionClosed }
158
+
159
+ it_behaves_like 'session death'
160
+ end
161
+ end
97
162
  end
98
163
  end
99
164
 
@@ -106,6 +171,64 @@ describe ZK::Client do
106
171
  @zk.session_passwd.should be_kind_of(String)
107
172
  end
108
173
  end
174
+
175
+ describe 'reopen' do
176
+ describe 'watchers' do
177
+ before do
178
+ @path = '/testwatchers'
179
+ @queue = Queue.new
180
+ end
181
+
182
+ after do
183
+ @zk.delete(@path)
184
+ end
185
+
186
+ def ensure_event_delivery!
187
+ @sub ||= @zk.event_handler.register(@path) do |event|
188
+ logger.debug { "got event: #{event.inspect}" }
189
+ @queue << event
190
+ end
191
+
192
+ @zk.exists?(@path, :watch => true).should be_false
193
+ @zk.create(@path, '')
194
+
195
+ logger.debug { "waiting for event delivery" }
196
+
197
+ wait_until(2) do
198
+ begin
199
+ @events << @queue.pop(true)
200
+ true
201
+ rescue ThreadError
202
+ false
203
+ end
204
+ end
205
+
206
+ # first watch delivered correctly
207
+ @events.length.should > 0
208
+ end
209
+
210
+ it %[should fire re-registered watchers after reopen (#9)] do
211
+ @events = []
212
+
213
+ logger.debug { "ensure event delivery" }
214
+ ensure_event_delivery!
215
+
216
+ logger.debug { "reopening connection" }
217
+ @zk.reopen
218
+
219
+ wait_until(2) { @zk.connected? }
220
+
221
+ logger.debug { "deleting path" }
222
+ @zk.delete(@path)
223
+
224
+ logger.debug { "clearing events" }
225
+ @events.clear
226
+
227
+ logger.debug { "taunt them a second time" }
228
+ ensure_event_delivery!
229
+ end
230
+ end
231
+ end
109
232
  end
110
233
 
111
234
 
data/zk.gemspec CHANGED
@@ -14,10 +14,6 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.add_runtime_dependency 'slyphon-zookeeper', '~> 0.3.0'
16
16
 
17
- s.add_development_dependency 'rspec', '~> 2.8.0'
18
- s.add_development_dependency 'flexmock', '~> 0.8.10'
19
- s.add_development_dependency 'ZenTest', '~> 4.5.0'
20
- s.add_development_dependency 'pry'
21
17
 
22
18
  s.files = `git ls-files`.split("\n")
23
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zk
3
3
  version: !ruby/object:Gem::Version
4
- hash: 47
4
+ hash: 45
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 8
9
- - 8
10
- version: 0.8.8
9
+ - 9
10
+ version: 0.8.9
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jonathan D. Simms
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2012-04-13 00:00:00 Z
19
+ date: 2012-04-21 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: slyphon-zookeeper
@@ -34,68 +34,6 @@ dependencies:
34
34
  version: 0.3.0
35
35
  type: :runtime
36
36
  version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: rspec
39
- prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ~>
44
- - !ruby/object:Gem::Version
45
- hash: 47
46
- segments:
47
- - 2
48
- - 8
49
- - 0
50
- version: 2.8.0
51
- type: :development
52
- version_requirements: *id002
53
- - !ruby/object:Gem::Dependency
54
- name: flexmock
55
- prerelease: false
56
- requirement: &id003 !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ~>
60
- - !ruby/object:Gem::Version
61
- hash: 43
62
- segments:
63
- - 0
64
- - 8
65
- - 10
66
- version: 0.8.10
67
- type: :development
68
- version_requirements: *id003
69
- - !ruby/object:Gem::Dependency
70
- name: ZenTest
71
- prerelease: false
72
- requirement: &id004 !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
75
- - - ~>
76
- - !ruby/object:Gem::Version
77
- hash: 43
78
- segments:
79
- - 4
80
- - 5
81
- - 0
82
- version: 4.5.0
83
- type: :development
84
- version_requirements: *id004
85
- - !ruby/object:Gem::Dependency
86
- name: pry
87
- prerelease: false
88
- requirement: &id005 !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ">="
92
- - !ruby/object:Gem::Version
93
- hash: 3
94
- segments:
95
- - 0
96
- version: "0"
97
- type: :development
98
- version_requirements: *id005
99
37
  description: |
100
38
  A high-level wrapper around the zookeeper driver
101
39
 
@@ -112,6 +50,7 @@ files:
112
50
  - .dotfiles/rspec-logging
113
51
  - .dotfiles/rvmrc
114
52
  - .gitignore
53
+ - .travis.yml
115
54
  - .yardopts
116
55
  - Gemfile
117
56
  - LICENSE
@@ -138,6 +77,7 @@ files:
138
77
  - lib/z_k/threadpool.rb
139
78
  - lib/z_k/version.rb
140
79
  - lib/zk.rb
80
+ - spec/informal/lock_with_dead_session.rb
141
81
  - spec/log4j.properties
142
82
  - spec/message_queue_spec.rb
143
83
  - spec/spec_helper.rb
@@ -189,6 +129,7 @@ signing_key:
189
129
  specification_version: 3
190
130
  summary: A high-level wrapper around the zookeeper driver
191
131
  test_files:
132
+ - spec/informal/lock_with_dead_session.rb
192
133
  - spec/log4j.properties
193
134
  - spec/message_queue_spec.rb
194
135
  - spec/spec_helper.rb