zkruby 3.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'server_helper'
2
+ require 'shared/binding'
3
+ require 'zkruby/rubyio'
4
+
5
+ describe ZooKeeper::RubyIO::Binding do
6
+ it_behaves_like "a zookeeper client binding"
7
+ end
8
+
@@ -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
@@ -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
+