zkruby 3.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/.gemtest +0 -0
- data/History.txt +18 -0
- data/Manifest.txt +39 -0
- data/README.rdoc +119 -0
- data/Rakefile +39 -0
- data/jute/jute.citrus +105 -0
- data/jute/lib/hoe/jute.rb +56 -0
- data/jute/lib/jute.rb +120 -0
- data/lib/em_zkruby.rb +4 -0
- data/lib/jute/zookeeper.rb +203 -0
- data/lib/zkruby.rb +4 -0
- data/lib/zkruby/bindata.rb +45 -0
- data/lib/zkruby/client.rb +608 -0
- data/lib/zkruby/enum.rb +108 -0
- data/lib/zkruby/eventmachine.rb +186 -0
- data/lib/zkruby/multi.rb +47 -0
- data/lib/zkruby/protocol.rb +182 -0
- data/lib/zkruby/rubyio.rb +310 -0
- data/lib/zkruby/session.rb +445 -0
- data/lib/zkruby/util.rb +141 -0
- data/lib/zkruby/zkruby.rb +27 -0
- data/spec/bindata_spec.rb +51 -0
- data/spec/enum_spec.rb +84 -0
- data/spec/eventmachine_spec.rb +50 -0
- data/spec/multi_spec.rb +93 -0
- data/spec/protocol_spec.rb +72 -0
- data/spec/recipe_helper.rb +68 -0
- data/spec/rubyio_spec.rb +8 -0
- data/spec/sequences_spec.rb +19 -0
- data/spec/server_helper.rb +45 -0
- data/spec/shared/auth.rb +40 -0
- data/spec/shared/basic.rb +180 -0
- data/spec/shared/binding.rb +33 -0
- data/spec/shared/chroot.rb +61 -0
- data/spec/shared/multi.rb +38 -0
- data/spec/shared/util.rb +56 -0
- data/spec/shared/watch.rb +49 -0
- data/spec/spec_helper.rb +12 -0
- data/src/jute/zookeeper.jute +288 -0
- data/yard_ext/enum_handler.rb +16 -0
- metadata +243 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Raw object protocol, very similar to EM's native ObjectProtocol
|
4
|
+
# Expects:
|
5
|
+
# #receive_data and #send_records to be invoked
|
6
|
+
# #receive_records and #send_data to be implemented
|
7
|
+
class ProtocolBindingSpec
|
8
|
+
include ZooKeeper::Protocol
|
9
|
+
attr_reader :data,:binding
|
10
|
+
|
11
|
+
def initialize(mock)
|
12
|
+
@data = ""
|
13
|
+
@binding = mock
|
14
|
+
end
|
15
|
+
|
16
|
+
def receive_records(io)
|
17
|
+
data = io.read(@binding.packet_size())
|
18
|
+
@binding.receive_records(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def send_data(data)
|
22
|
+
@data << data
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def length_encode(data)
|
27
|
+
[data.length,data].pack("NA*")
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ZooKeeper::Protocol do
|
31
|
+
|
32
|
+
before :each do
|
33
|
+
@conn = ProtocolBindingSpec.new(mock("protocol binding"))
|
34
|
+
end
|
35
|
+
|
36
|
+
context "recieving data" do
|
37
|
+
it "should handle self contained packets" do
|
38
|
+
data = "a complete packet"
|
39
|
+
@conn.binding.stub(:packet_size).and_return(data.length)
|
40
|
+
@conn.binding.should_receive(:receive_records).with(data)
|
41
|
+
@conn.receive_data(length_encode(data))
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should handle reply packets over multiple chunks" do
|
45
|
+
data = "a complete packet that will be split into chunks"
|
46
|
+
@conn.binding.stub(:packet_size).and_return(data.length)
|
47
|
+
@conn.binding.should_receive(:receive_records).with(data)
|
48
|
+
chunks = length_encode(data).scan(/.{1,12}/)
|
49
|
+
chunks.each { |c| @conn.receive_data(c) }
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should handle replies containing multiple packets" do
|
53
|
+
p1 = "first packet"
|
54
|
+
p2 = "second packet"
|
55
|
+
@conn.binding.stub(:packet_size).and_return(p1.length,p2.length)
|
56
|
+
@conn.binding.should_receive(:receive_records).with(p1).ordered
|
57
|
+
@conn.binding.should_receive(:receive_records).with(p2).ordered
|
58
|
+
data = length_encode(p1) + length_encode(p2)
|
59
|
+
@conn.receive_data(data)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "sending data" do
|
64
|
+
it "should length encode the data" do
|
65
|
+
mock_record = mock("a mock record")
|
66
|
+
mock_record.stub(:to_binary_s).and_return("a binary string")
|
67
|
+
@conn.send_records(mock_record)
|
68
|
+
@conn.data.should == [15,"a binary string"].pack("NA15")
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#Helper classes for testing recipes against a mockable zookeeper client
|
4
|
+
#with pseudo asynchronous behaviour
|
5
|
+
|
6
|
+
module ZooKeeper
|
7
|
+
# We need to run callback blocks for results
|
8
|
+
# in a sequence and inject watches in the middle
|
9
|
+
class MockCallback
|
10
|
+
attr_accessor :errback, :callback, :err, :results
|
11
|
+
def initialize(init_callback)
|
12
|
+
@callback = init_callback
|
13
|
+
end
|
14
|
+
|
15
|
+
def invoke()
|
16
|
+
cb,args = err ? [errback,[err]] : [callback,results]
|
17
|
+
cb.call(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# This harness wraps a double on which the calls to a zk client can be stubbed
|
22
|
+
# with return values or exceptions (ZooKeeper::Error)
|
23
|
+
# If the recipe under test makes an asynchronous call to the mock client, then
|
24
|
+
# the callback block is captured alongside the stub result and put in a queue
|
25
|
+
# for later processing via #run_queue
|
26
|
+
# If the recipe makes a synchronous style call then the existing queue will be run
|
27
|
+
# before the stub result is returned directly.
|
28
|
+
class MockClient
|
29
|
+
attr_reader :double
|
30
|
+
def initialize(double)
|
31
|
+
@queue = []
|
32
|
+
# we delegate all stub methods to our double.
|
33
|
+
# if a block is supplied we return an op with the callback and result
|
34
|
+
# the op can take an err back. When the tests finish running
|
35
|
+
# we invoke the methods in the queue, repeatedly until the queue is empty
|
36
|
+
@double = double
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(meth,*args,&callback)
|
40
|
+
if callback
|
41
|
+
cb = ZooKeeper::MockCallback.new(callback)
|
42
|
+
begin
|
43
|
+
cb.results = @double.send(meth,*args)
|
44
|
+
rescue ZooKeeper::Error => ex
|
45
|
+
cb.err = ex.to_sym
|
46
|
+
end
|
47
|
+
@queue << cb
|
48
|
+
ZooKeeper::AsyncOp.new(cb)
|
49
|
+
else
|
50
|
+
# we won't get the return from a synchronous call until
|
51
|
+
# any pending asynchronous calls have completed
|
52
|
+
# only runs the current queued items, any additional activity
|
53
|
+
# queued as a result of a callback goes into a new queue
|
54
|
+
queued_cbs = @queue
|
55
|
+
@queue = []
|
56
|
+
queued_cbs.each { |cb| cb.invoke() }
|
57
|
+
@double.send(meth,*args) unless callback
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_queue()
|
62
|
+
until @queue.empty?
|
63
|
+
cb = @queue.shift
|
64
|
+
cb.invoke()
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end #ZooKeeper
|
data/spec/rubyio_spec.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ZooKeeper do
|
4
|
+
it "should convert a path to a prefix and the sequence id" do
|
5
|
+
|
6
|
+
ZK.path_to_seq("/a/path/with-0000000001/a/sequence-0000000002").should == [ "/a/path/with-0000000001/a/sequence-",2 ]
|
7
|
+
ZK.path_to_seq("/tricky/9990000000004").should == [ "/tricky/999", 4 ]
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should graciously handle a path without a sequence" do
|
11
|
+
ZK.path_to_seq("/a/path").should == ["/a/path",nil]
|
12
|
+
ZK.path_to_seq("/a/0000000001/").should == ["/a/0000000001/",nil]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should convert to a prefix and sequence id to a path" do
|
16
|
+
ZK.seq_to_path("/a/path",345).should == "/a/path0000000345"
|
17
|
+
ZK.seq_to_path("/a/path00/",123).should == "/a/path00/0000000123"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ZooKeeperServerHelper
|
4
|
+
|
5
|
+
include Slf4r::Logger
|
6
|
+
|
7
|
+
def restart_cluster(delay=0)
|
8
|
+
system("../../bin/zkServer.sh stop >> zk.out")
|
9
|
+
Kernel::sleep(delay) if delay > 0
|
10
|
+
if (::RUBY_PLATFORM == "java")
|
11
|
+
#in JRuby 1.6.3 system does not return
|
12
|
+
system("../../bin/zkServer.sh start >> zk.out &")
|
13
|
+
else
|
14
|
+
system("../../bin/zkServer.sh start >> zk.out")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_addresses()
|
19
|
+
"localhost:2181"
|
20
|
+
end
|
21
|
+
|
22
|
+
def safe_close(zk)
|
23
|
+
zk.close()
|
24
|
+
rescue ZooKeeper::Error => ex
|
25
|
+
puts "Ignoring close exception #{ex}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def connect(options = {})
|
29
|
+
ZooKeeper.connect("localhost:2181",options)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
include ZooKeeperServerHelper
|
35
|
+
|
36
|
+
restart_cluster()
|
37
|
+
sleep(3)
|
38
|
+
require 'net/telnet'
|
39
|
+
t = Net::Telnet.new("Host" => "localhost", "Port" => 2181)
|
40
|
+
properties = t.cmd("mntr")
|
41
|
+
|
42
|
+
RSpec.configure do |c|
|
43
|
+
#Exclude multi unless we are on a 3.4 server
|
44
|
+
c.filter_run_excluding :multi => true unless properties
|
45
|
+
end
|
data/spec/shared/auth.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
shared_examples_for "authentication" do
|
2
|
+
|
3
|
+
describe "Authentication" do
|
4
|
+
|
5
|
+
around(:each) do |example|
|
6
|
+
@path = "/zkruby/rspec-auth"
|
7
|
+
@zk = connect(:scheme => "digest", :auth => "myuser:mypass", :binding => binding)
|
8
|
+
@zk.create(@path,"Auth Test",ZK::ACL_CREATOR_ALL,:ephemeral)
|
9
|
+
|
10
|
+
example.run
|
11
|
+
|
12
|
+
safe_close(@zk)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
it "should not allow unauthenticated access" do
|
17
|
+
zk2 = connect(:binding => binding)
|
18
|
+
lambda { zk2.get(@path) }.should raise_error(ZooKeeper::Error)
|
19
|
+
zk2.close()
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should allow authenticated access" do
|
23
|
+
zk2 = connect(:scheme => "digest", :auth => "myuser:mypass", :binding => binding)
|
24
|
+
stat,data = zk2.get(@path)
|
25
|
+
data.should == "Auth Test"
|
26
|
+
zk2.close()
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not allow access with bad password" do
|
30
|
+
zk2 = connect(:scheme => "digest", :auth => "myuser:badpass", :binding => binding)
|
31
|
+
lambda { zk2.get(@path) }.should raise_error(ZooKeeper::Error)
|
32
|
+
zk2.close()
|
33
|
+
end
|
34
|
+
|
35
|
+
# Digest doesn't actually validate credentials, just ensures they match against the ACL
|
36
|
+
# We'll need to test this with a mock Binding
|
37
|
+
it "should get to auth_failed if invalid credentials supplied"
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
shared_examples_for "basic integration" do
|
2
|
+
|
3
|
+
before(:each) do
|
4
|
+
@zk.create("/zkruby","node for zk ruby testing",ZK::ACL_OPEN_UNSAFE) unless @zk.exists?("/zkruby")
|
5
|
+
end
|
6
|
+
|
7
|
+
it "should return a stat for the root path" do
|
8
|
+
stat = @zk.stat("/")
|
9
|
+
stat.should be_a ZooKeeper::Data::Stat
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should asynchronously return a stat for the root path" do
|
13
|
+
op = @zk.stat("/") do |stat|
|
14
|
+
stat.should be_a ZooKeeper::Data::Stat
|
15
|
+
:success
|
16
|
+
end
|
17
|
+
|
18
|
+
op.value.should == :success
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should perform ZooKeeper CRUD" do
|
22
|
+
path = @zk.create("/zkruby/rspec","someData",ZK::ACL_OPEN_UNSAFE,:ephemeral)
|
23
|
+
path.should == "/zkruby/rspec"
|
24
|
+
@zk.exists?("/zkruby/rspec").should be_true
|
25
|
+
stat,data = @zk.get("/zkruby/rspec")
|
26
|
+
stat.should be_a ZooKeeper::Data::Stat
|
27
|
+
data.should == "someData"
|
28
|
+
new_stat = @zk.set("/zkruby/rspec","different data",stat.version)
|
29
|
+
new_stat.should be_a ZooKeeper::Data::Stat
|
30
|
+
stat,data = @zk.get("/zkruby/rspec")
|
31
|
+
data.should == "different data"
|
32
|
+
cstat,children = @zk.children("/zkruby")
|
33
|
+
children.should include("rspec")
|
34
|
+
@zk.delete("/zkruby/rspec",stat.version)
|
35
|
+
@zk.exists?("/zkruby/rspec").should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should accept -1 to delete any version" do
|
39
|
+
path = @zk.create("/zkruby/rspec","someData",ZK::ACL_OPEN_UNSAFE,:ephemeral)
|
40
|
+
@zk.delete("/zkruby/rspec",-1)
|
41
|
+
@zk.exists?("/zkruby/rspec").should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
context "exceptions" do
|
45
|
+
|
46
|
+
it "should raise ZK::Error for synchronous method" do
|
47
|
+
begin
|
48
|
+
get_caller = caller
|
49
|
+
@zk.get("/anunknownpath")
|
50
|
+
fail "Expected no node error"
|
51
|
+
rescue ZooKeeper::Error => ex
|
52
|
+
# only because JRuby 1.9 doesn't support the === syntax for exceptions
|
53
|
+
ZooKeeper::Error::NO_NODE.should === ex
|
54
|
+
ex.message.should =~ /\/anunknownpath/
|
55
|
+
skip = if defined?(JRUBY_VERSION) then 2 else 1 end
|
56
|
+
ex.backtrace[skip..-1].should == get_caller
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should capture ZK error for asynchronous method" do
|
61
|
+
get_caller = caller
|
62
|
+
op = @zk.get("/an/unknown/path") { raise "callback invoked unexpectedly" }
|
63
|
+
|
64
|
+
begin
|
65
|
+
op.value
|
66
|
+
fail "Expected no node error"
|
67
|
+
rescue ZooKeeper::Error => ex
|
68
|
+
ZooKeeper::Error::NO_NODE.should === ex
|
69
|
+
ex.message.should =~ /\/an\/unknown\/path/
|
70
|
+
ex.backtrace[1..-1].should == get_caller
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should call the error call back for asynchronous errors" do
|
75
|
+
op = @zk.get("/an/unknown/path") do
|
76
|
+
:callback_invoked_unexpectedly
|
77
|
+
end
|
78
|
+
|
79
|
+
op.on_error do |err|
|
80
|
+
case err
|
81
|
+
when ZK::Error::NO_NODE
|
82
|
+
:found_no_node_error
|
83
|
+
else
|
84
|
+
raise err
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
op.value.should == :found_no_node_error
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should capture exceptions from asynchronous callback" do
|
92
|
+
async_caller = nil
|
93
|
+
op = @zk.exists?("/zkruby") do |stat|
|
94
|
+
raise "oops"
|
95
|
+
end
|
96
|
+
|
97
|
+
begin
|
98
|
+
op.value
|
99
|
+
fail "Expected RuntimeError"
|
100
|
+
rescue RuntimeError => ex
|
101
|
+
ex.message.should =~ /oops/
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
context "anti herd-effect features" do
|
108
|
+
it "should randomly shuffle the address list"
|
109
|
+
|
110
|
+
it "should randomly delay reconnections within one seventh of the timeout"
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
context "auto reconnect" do
|
115
|
+
|
116
|
+
it "should stay connected" do
|
117
|
+
sleep(@zk.timeout * 2.0)
|
118
|
+
@zk.exists?("/zkruby").should be_true
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
it "should seamlessly reconnect within the timeout period" do
|
123
|
+
watcher = mock("Watcher").as_null_object
|
124
|
+
watcher.should_receive(:process_watch).with(ZK::KeeperState::DISCONNECTED,nil,ZK::WatchEvent::NONE)
|
125
|
+
watcher.should_receive(:process_watch).with(ZK::KeeperState::CONNECTED,nil,ZK::WatchEvent::NONE)
|
126
|
+
watcher.should_not_receive(:process_watch).with(ZK::KeeperState::EXPIRED,nil,ZK::WatchEvent::NONE)
|
127
|
+
@zk.watcher = watcher
|
128
|
+
restart_cluster(2)
|
129
|
+
@zk.exists?("/zkruby").should be_true
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should eventually expire the session" do
|
133
|
+
watcher = mock("Watcher").as_null_object
|
134
|
+
watcher.should_receive(:process_watch).with(ZK::KeeperState::DISCONNECTED,nil,ZK::WatchEvent::NONE)
|
135
|
+
watcher.should_receive(:process_watch).with(ZK::KeeperState::EXPIRED,nil,ZK::WatchEvent::NONE)
|
136
|
+
@zk.watcher = watcher
|
137
|
+
restart_cluster(@zk.timeout * 2.0)
|
138
|
+
lambda { @zk.exists?("/zkruby") }.should raise_error(ZooKeeper::Error)
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
context "mixed sync and async calls" do
|
145
|
+
|
146
|
+
before(:each) do
|
147
|
+
@zk.delete("/zkruby/sync_async", -1) if @zk.exists?("/zkruby/sync_async")
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should handle a synchronous call inside an asynchronous callback" do
|
151
|
+
op = @zk.create("/zkruby/sync_async","somedata",ZK::ACL_OPEN_UNSAFE) do
|
152
|
+
ZK.current.should equal(@zk)
|
153
|
+
stat, data = @zk.get("/zkruby/sync_async")
|
154
|
+
data
|
155
|
+
end
|
156
|
+
|
157
|
+
op.value.should == "somedata"
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should handle a synchronous call inside an asynchronous error callback" do
|
161
|
+
|
162
|
+
op = @zk.create("/zkruby/some_never_created_node/test","test_data",ZK::ACL_OPEN_UNSAFE) do
|
163
|
+
:should_not_get_here
|
164
|
+
end
|
165
|
+
|
166
|
+
op.on_error do |err|
|
167
|
+
ZK.current.should equal(@zk)
|
168
|
+
case err
|
169
|
+
when ZK::Error::NO_NODE
|
170
|
+
stat,data = @zk.get("/zkruby")
|
171
|
+
:success
|
172
|
+
else
|
173
|
+
raise err
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
op.value.should == :success
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'server_helper'
|
2
|
+
require 'shared/basic'
|
3
|
+
require 'shared/util'
|
4
|
+
require 'shared/chroot'
|
5
|
+
require 'shared/watch'
|
6
|
+
require 'shared/multi'
|
7
|
+
require 'shared/auth'
|
8
|
+
|
9
|
+
shared_examples_for "a zookeeper client binding" do
|
10
|
+
|
11
|
+
let (:binding) { described_class }
|
12
|
+
|
13
|
+
context "A local connection" do
|
14
|
+
|
15
|
+
around(:each) do |example|
|
16
|
+
ZooKeeper.connect(get_addresses(),:binding => binding) do | zk |
|
17
|
+
@zk = zk
|
18
|
+
ZooKeeper.current.should == zk
|
19
|
+
example.run
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include_examples("basic integration")
|
24
|
+
include_examples("util recipes")
|
25
|
+
include_examples("multi")
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
include_examples("authentication")
|
30
|
+
include_examples("chrooted connection")
|
31
|
+
include_examples("watches")
|
32
|
+
end
|
33
|
+
|