ralphrodkey-kafka-rb 0.0.15

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,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