ralphrodkey-kafka-rb 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one or more
2
+ # contributor license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright ownership.
4
+ # The ASF licenses this file to You under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with
6
+ # the License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ require File.dirname(__FILE__) + '/spec_helper'
16
+
17
+ describe Consumer do
18
+
19
+ before(:each) do
20
+ @mocked_socket = mock(TCPSocket)
21
+ TCPSocket.stub!(:new).and_return(@mocked_socket) # don't use a real socket
22
+ @consumer = Consumer.new(:offset => 0)
23
+ end
24
+
25
+ describe "Kafka Consumer" do
26
+
27
+ it "should have a Kafka::RequestType::FETCH" do
28
+ Kafka::RequestType::FETCH.should eql(1)
29
+ @consumer.should respond_to(:request_type)
30
+ end
31
+
32
+ it "should have a topic and a partition" do
33
+ @consumer.should respond_to(:topic)
34
+ @consumer.should respond_to(:partition)
35
+ end
36
+
37
+ it "should have a polling option, and a default value" do
38
+ Consumer::DEFAULT_POLLING_INTERVAL.should eql(2)
39
+ @consumer.should respond_to(:polling)
40
+ @consumer.polling.should eql(2)
41
+ end
42
+
43
+ it "should set a topic and partition on initialize" do
44
+ @consumer = Consumer.new({ :host => "localhost", :port => 9092, :topic => "testing" })
45
+ @consumer.topic.should eql("testing")
46
+ @consumer.partition.should eql(0)
47
+ @consumer = Consumer.new({ :topic => "testing", :partition => 3 })
48
+ @consumer.partition.should eql(3)
49
+ end
50
+
51
+ it "should set default host and port if none is specified" do
52
+ @consumer = Consumer.new
53
+ @consumer.host.should eql("localhost")
54
+ @consumer.port.should eql(9092)
55
+ end
56
+
57
+ it "should not have a default offset but be able to set it" do
58
+ @consumer = Consumer.new
59
+ @consumer.offset.should be_nil
60
+ @consumer = Consumer.new({ :offset => 1111 })
61
+ @consumer.offset.should eql(1111)
62
+ end
63
+
64
+ it "should have a max size" do
65
+ Consumer::MAX_SIZE.should eql(1048576)
66
+ @consumer.max_size.should eql(1048576)
67
+ end
68
+
69
+ it "should return the size of the request" do
70
+ @consumer.topic = "someothertopicname"
71
+ @consumer.encoded_request_size.should eql([38].pack("N"))
72
+ end
73
+
74
+ it "should encode a request to consume" do
75
+ bytes = [Kafka::RequestType::FETCH].pack("n") + ["test".length].pack("n") + "test" + [0].pack("N") + [0].pack("q").reverse + [Kafka::Consumer::MAX_SIZE].pack("N")
76
+ @consumer.encode_request(Kafka::RequestType::FETCH, "test", 0, 0, Kafka::Consumer::MAX_SIZE).should eql(bytes)
77
+ end
78
+
79
+ it "should read the response data" do
80
+ bytes = [0].pack("n") + [1120192889].pack("N") + "ale"
81
+ @mocked_socket.should_receive(:read).and_return([9].pack("N"))
82
+ @mocked_socket.should_receive(:read).with(9).and_return(bytes)
83
+ @consumer.read_data_response.should eql(bytes[2,7])
84
+ end
85
+
86
+ it "should send a consumer request" do
87
+ @consumer.stub!(:encoded_request_size).and_return(666)
88
+ @consumer.stub!(:encode_request).and_return("someencodedrequest")
89
+ @consumer.should_receive(:write).with("someencodedrequest").exactly(:once).and_return(true)
90
+ @consumer.should_receive(:write).with(666).exactly(:once).and_return(true)
91
+ @consumer.send_consume_request.should eql(true)
92
+ end
93
+
94
+ it "should consume messages" do
95
+ @consumer.should_receive(:send_consume_request).and_return(true)
96
+ @consumer.should_receive(:read_data_response).and_return("")
97
+ @consumer.consume.should eql([])
98
+ end
99
+
100
+ it "should loop and execute a block with the consumed messages" do
101
+ @consumer.stub!(:consume).and_return([mock(Kafka::Message)])
102
+ messages = []
103
+ messages.should_receive(:<<).exactly(:once).and_return([])
104
+ @consumer.loop do |message|
105
+ messages << message
106
+ break # we don't wanna loop forever on the test
107
+ end
108
+ end
109
+
110
+ it "should loop (every N seconds, configurable on polling attribute), and execute a block with the consumed messages" do
111
+ @consumer = Consumer.new({ :polling => 1 })
112
+ @consumer.stub!(:consume).and_return([mock(Kafka::Message)])
113
+ messages = []
114
+ messages.should_receive(:<<).exactly(:twice).and_return([])
115
+ executed_times = 0
116
+ @consumer.loop do |message|
117
+ messages << message
118
+ executed_times += 1
119
+ break if executed_times >= 2 # we don't wanna loop forever on the test, only 2 seconds
120
+ end
121
+
122
+ executed_times.should eql(2)
123
+ end
124
+
125
+ it "should fetch initial offset if no offset is given" do
126
+ @consumer = Consumer.new
127
+ @consumer.should_receive(:fetch_latest_offset).exactly(:once).and_return(1000)
128
+ @consumer.should_receive(:send_consume_request).and_return(true)
129
+ @consumer.should_receive(:read_data_response).and_return("")
130
+ @consumer.consume
131
+ @consumer.offset.should eql(1000)
132
+ end
133
+
134
+ it "should encode an offset request" do
135
+ bytes = [Kafka::RequestType::OFFSETS].pack("n") + ["test".length].pack("n") + "test" + [0].pack("N") + [-1].pack("q").reverse + [Kafka::Consumer::MAX_OFFSETS].pack("N")
136
+ @consumer.encode_request(Kafka::RequestType::OFFSETS, "test", 0, -1, Kafka::Consumer::MAX_OFFSETS).should eql(bytes)
137
+ end
138
+
139
+ it "should parse an offsets response" do
140
+ bytes = [0].pack("n") + [1].pack('N') + [21346].pack('q').reverse
141
+ @mocked_socket.should_receive(:read).and_return([14].pack("N"))
142
+ @mocked_socket.should_receive(:read).and_return(bytes)
143
+ @consumer.read_offsets_response.should eql(21346)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,251 @@
1
+ # encoding: utf-8
2
+
3
+ # Licensed to the Apache Software Foundation (ASF) under one or more
4
+ # contributor license agreements. See the NOTICE file distributed with
5
+ # this work for additional information regarding copyright ownership.
6
+ # The ASF licenses this file to You under the Apache License, Version 2.0
7
+ # (the "License"); you may not use this file except in compliance with
8
+ # the License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ require File.dirname(__FILE__) + '/spec_helper'
18
+
19
+ describe Encoder do
20
+ def check_message(bytes, message)
21
+ encoded = [message.magic].pack("C") + [message.calculate_checksum].pack("N") + message.payload
22
+ encoded = [encoded.length].pack("N") + encoded
23
+ bytes.should == encoded
24
+ end
25
+
26
+ describe "Message Encoding" do
27
+ it "should encode a message" do
28
+ message = Kafka::Message.new("alejandro")
29
+ check_message(described_class.message(message), message)
30
+ end
31
+
32
+ it "should encode an empty message" do
33
+ message = Kafka::Message.new
34
+ check_message(described_class.message(message), message)
35
+ end
36
+
37
+ it "should encode strings containing non-ASCII characters" do
38
+ message = Kafka::Message.new("ümlaut")
39
+ encoded = described_class.message(message)
40
+ message = Kafka::Message.parse_from(encoded).messages.first
41
+ if RUBY_VERSION[0,3] == "1.8" # Use old iconv on Ruby 1.8 for encoding
42
+ #ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
43
+ #ic.iconv(message.payload).should eql("ümlaut")
44
+ message.payload.should eql("ümlaut")
45
+ else
46
+ message.payload.force_encoding(Encoding::UTF_8).should eql("ümlaut")
47
+ end
48
+ end
49
+
50
+ it "should encode strings containing non-ASCII characters" do
51
+ message = Kafka::Message.new("\214")
52
+ encoded = described_class.message(message)
53
+ message = Kafka::Message.parse_from(encoded).messages.first
54
+ if RUBY_VERSION[0,3] == "1.8"
55
+ message.payload.should eql("\214")
56
+ else
57
+ message.payload.force_encoding(Encoding::UTF_8).should eql("\214")
58
+ end
59
+ end
60
+ end
61
+
62
+ describe :compression do
63
+ before do
64
+ @message = Kafka::Message.new "foo"
65
+ end
66
+
67
+ it "should default to no compression" do
68
+ msg = "foo"
69
+ checksum = Zlib.crc32 msg
70
+ magic = 0
71
+ msg_size = 5 + msg.size
72
+ raw = [msg_size, magic, checksum, msg].pack "NCNa#{msg.size}"
73
+
74
+ Encoder.message(@message).should == raw
75
+ end
76
+
77
+ it "should support GZip compression" do
78
+ buffer = StringIO.new
79
+ gz = Zlib::GzipWriter.new buffer, nil, nil
80
+ gz.write "foo"
81
+ gz.close
82
+ buffer.rewind
83
+ msg = buffer.string
84
+ checksum = Zlib.crc32 msg
85
+ magic = 1
86
+ attrs = 1
87
+ msg_size = 6 + msg.size
88
+ raw = [msg_size, magic, attrs, checksum, msg].pack "NCCNa#{msg.size}"
89
+ Encoder.message(@message, 1).should == raw
90
+ end
91
+
92
+ if Object.const_defined? "Snappy"
93
+ it "should support Snappy compression" do
94
+ buffer = StringIO.new
95
+ Snappy::Writer.new buffer do |w|
96
+ w << "foo"
97
+ end
98
+ buffer.rewind
99
+ msg = buffer.string
100
+ checksum = Zlib.crc32 msg
101
+ magic = 1
102
+ attrs = 2
103
+ msg_size = 6 + msg.size
104
+ raw = [msg_size, magic, attrs, checksum, msg].pack "NCCNa#{msg.size}"
105
+
106
+ Encoder.message(@message, 2).should == raw
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "produce" do
112
+ it "should binary encode an empty request" do
113
+ bytes = described_class.produce("test", 0, [])
114
+ bytes.length.should eql(20)
115
+ bytes.should eql("\000\000\000\020\000\000\000\004test\000\000\000\000\000\000\000\000")
116
+ end
117
+
118
+ it "should binary encode a request with a message, using a specific wire format" do
119
+ message = Kafka::Message.new("ale")
120
+ bytes = described_class.produce("test", 3, message)
121
+ data_size = bytes[0, 4].unpack("N").shift
122
+ request_id = bytes[4, 2].unpack("n").shift
123
+ topic_length = bytes[6, 2].unpack("n").shift
124
+ topic = bytes[8, 4]
125
+ partition = bytes[12, 4].unpack("N").shift
126
+ messages_length = bytes[16, 4].unpack("N").shift
127
+ messages = bytes[20, messages_length]
128
+
129
+ bytes.length.should eql(32)
130
+ data_size.should eql(28)
131
+ request_id.should eql(0)
132
+ topic_length.should eql(4)
133
+ topic.should eql("test")
134
+ partition.should eql(3)
135
+ messages_length.should eql(12)
136
+ end
137
+ end
138
+
139
+ describe "message_set" do
140
+ it "should compress messages into a message set" do
141
+ message_one = Kafka::Message.new "foo"
142
+ message_two = Kafka::Message.new "bar"
143
+ bytes = described_class.message_set [message_one, message_two], Kafka::Message::GZIP_COMPRESSION
144
+
145
+ messages = Kafka::Message.parse_from bytes
146
+ messages.should be_a Kafka::Message::MessageSet
147
+ messages.messages.size.should == 2
148
+
149
+ messages.messages[0].should be_a Kafka::Message
150
+ messages.messages[0].payload.should == "foo"
151
+ messages.messages[1].should be_a Kafka::Message
152
+ messages.messages[1].payload.should == "bar"
153
+ end
154
+ end
155
+
156
+ describe "multiproduce" do
157
+ it "encodes an empty request" do
158
+ bytes = described_class.multiproduce([])
159
+ bytes.length.should == 8
160
+ bytes.should == "\x00\x00\x00\x04\x00\x03\x00\x00"
161
+ end
162
+
163
+ it "encodes a request with a single topic/partition" do
164
+ message = Kafka::Message.new("ale")
165
+ bytes = described_class.multiproduce(Kafka::ProducerRequest.new("test", message))
166
+
167
+ req_length = bytes[0, 4].unpack("N").shift
168
+ req_type = bytes[4, 2].unpack("n").shift
169
+ tp_count = bytes[6, 2].unpack("n").shift
170
+
171
+ req_type.should == Kafka::RequestType::MULTIPRODUCE
172
+ tp_count.should == 1
173
+
174
+ topic_length = bytes[8, 2].unpack("n").shift
175
+ topic = bytes[10, 4]
176
+ partition = bytes[14, 4].unpack("N").shift
177
+ messages_length = bytes[18, 4].unpack("N").shift
178
+ messages_data = bytes[22, messages_length]
179
+
180
+ topic_length.should == 4
181
+ topic.should == "test"
182
+ partition.should == 0
183
+ messages_length.should == 12
184
+ check_message(messages_data, message)
185
+ end
186
+
187
+ it "encodes a request with a single topic/partition but multiple messages" do
188
+ messages = [Kafka::Message.new("ale"), Kafka::Message.new("beer")]
189
+ bytes = described_class.multiproduce(Kafka::ProducerRequest.new("test", messages))
190
+
191
+ req_length = bytes[0, 4].unpack("N").shift
192
+ req_type = bytes[4, 2].unpack("n").shift
193
+ tp_count = bytes[6, 2].unpack("n").shift
194
+
195
+ req_type.should == Kafka::RequestType::MULTIPRODUCE
196
+ tp_count.should == 1
197
+
198
+ topic_length = bytes[8, 2].unpack("n").shift
199
+ topic = bytes[10, 4]
200
+ partition = bytes[14, 4].unpack("N").shift
201
+ messages_length = bytes[18, 4].unpack("N").shift
202
+ messages_data = bytes[22, messages_length]
203
+
204
+ topic_length.should == 4
205
+ topic.should == "test"
206
+ partition.should == 0
207
+ messages_length.should == 25
208
+ check_message(messages_data[0, 12], messages[0])
209
+ check_message(messages_data[12, 13], messages[1])
210
+ end
211
+
212
+ it "encodes a request with multiple topic/partitions" do
213
+ messages = [Kafka::Message.new("ale"), Kafka::Message.new("beer")]
214
+ bytes = described_class.multiproduce([
215
+ Kafka::ProducerRequest.new("test", messages[0]),
216
+ Kafka::ProducerRequest.new("topic", messages[1], :partition => 1),
217
+ ])
218
+
219
+ req_length = bytes[0, 4].unpack("N").shift
220
+ req_type = bytes[4, 2].unpack("n").shift
221
+ tp_count = bytes[6, 2].unpack("n").shift
222
+
223
+ req_type.should == Kafka::RequestType::MULTIPRODUCE
224
+ tp_count.should == 2
225
+
226
+ topic_length = bytes[8, 2].unpack("n").shift
227
+ topic = bytes[10, 4]
228
+ partition = bytes[14, 4].unpack("N").shift
229
+ messages_length = bytes[18, 4].unpack("N").shift
230
+ messages_data = bytes[22, 12]
231
+
232
+ topic_length.should == 4
233
+ topic.should == "test"
234
+ partition.should == 0
235
+ messages_length.should == 12
236
+ check_message(messages_data[0, 12], messages[0])
237
+
238
+ topic_length = bytes[34, 2].unpack("n").shift
239
+ topic = bytes[36, 5]
240
+ partition = bytes[41, 4].unpack("N").shift
241
+ messages_length = bytes[45, 4].unpack("N").shift
242
+ messages_data = bytes[49, 13]
243
+
244
+ topic_length.should == 5
245
+ topic.should == "topic"
246
+ partition.should == 1
247
+ messages_length.should == 13
248
+ check_message(messages_data[0, 13], messages[1])
249
+ end
250
+ end
251
+ end
data/spec/io_spec.rb ADDED
@@ -0,0 +1,88 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one or more
2
+ # contributor license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright ownership.
4
+ # The ASF licenses this file to You under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with
6
+ # the License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ require File.dirname(__FILE__) + '/spec_helper'
16
+
17
+ class IOTest
18
+ include Kafka::IO
19
+ end
20
+
21
+ describe IO do
22
+
23
+ before(:each) do
24
+ @mocked_socket = mock(TCPSocket)
25
+ TCPSocket.stub!(:new).and_return(@mocked_socket) # don't use a real socket
26
+ @io = IOTest.new
27
+ @io.connect("somehost", 9093)
28
+ end
29
+
30
+ describe "default methods" do
31
+ it "has a socket, a host and a port" do
32
+ [:socket, :host, :port].each do |m|
33
+ @io.should respond_to(m.to_sym)
34
+ end
35
+ end
36
+
37
+ it "raises an exception if no host and port is specified" do
38
+ lambda {
39
+ io = IOTest.new
40
+ io.connect
41
+ }.should raise_error(ArgumentError)
42
+ end
43
+
44
+ it "should remember the port and host on connect" do
45
+ @io.connect("somehost", 9093)
46
+ @io.host.should eql("somehost")
47
+ @io.port.should eql(9093)
48
+ end
49
+
50
+ it "should write to a socket" do
51
+ data = "some data"
52
+ @mocked_socket.should_receive(:write).with(data).and_return(9)
53
+ @io.write(data).should eql(9)
54
+ end
55
+
56
+ it "should read from a socket" do
57
+ length = 200
58
+ @mocked_socket.should_receive(:read).with(length).and_return("foo")
59
+ @io.read(length)
60
+ end
61
+
62
+ it "should disconnect on a timeout when reading from a socket (to aviod protocol desync state)" do
63
+ length = 200
64
+ @mocked_socket.should_receive(:read).with(length).and_raise(Errno::EAGAIN)
65
+ @io.should_receive(:disconnect)
66
+ lambda { @io.read(length) }.should raise_error(Kafka::SocketError)
67
+ end
68
+
69
+ it "should disconnect" do
70
+ @io.should respond_to(:disconnect)
71
+ @mocked_socket.should_receive(:close).and_return(nil)
72
+ @io.disconnect
73
+ end
74
+
75
+ it "should reconnect" do
76
+ TCPSocket.should_receive(:new)
77
+ @io.reconnect
78
+ end
79
+
80
+ it "should disconnect on a broken pipe error" do
81
+ [Errno::ECONNABORTED, Errno::EPIPE, Errno::ECONNRESET].each do |error|
82
+ @mocked_socket.should_receive(:write).exactly(:once).and_raise(error)
83
+ @mocked_socket.should_receive(:close).exactly(:once).and_return(nil)
84
+ lambda { @io.write("some data to send") }.should raise_error(Kafka::SocketError)
85
+ end
86
+ end
87
+ end
88
+ end