zkruby 3.4.3
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.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
|
+
|