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.
@@ -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
+