leffen-kafka-rb 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +202 -0
- data/README.md +123 -0
- data/Rakefile +40 -0
- data/bin/leffen-kafka-consumer +6 -0
- data/bin/leffen-kafka-publish +6 -0
- data/lib/kafka.rb +40 -0
- data/lib/kafka/batch.rb +28 -0
- data/lib/kafka/cli.rb +170 -0
- data/lib/kafka/consumer.rb +104 -0
- data/lib/kafka/encoder.rb +59 -0
- data/lib/kafka/error_codes.rb +35 -0
- data/lib/kafka/io.rb +57 -0
- data/lib/kafka/message.rb +209 -0
- data/lib/kafka/multi_producer.rb +35 -0
- data/lib/kafka/producer.rb +42 -0
- data/lib/kafka/producer_request.rb +26 -0
- data/lib/kafka/request_type.rb +23 -0
- data/lib/leffen-kafka.rb +16 -0
- data/spec/batch_spec.rb +35 -0
- data/spec/cli_spec.rb +133 -0
- data/spec/consumer_spec.rb +146 -0
- data/spec/encoder_spec.rb +251 -0
- data/spec/io_spec.rb +88 -0
- data/spec/kafka_spec.rb +20 -0
- data/spec/message_spec.rb +227 -0
- data/spec/multi_producer_spec.rb +74 -0
- data/spec/producer_request_spec.rb +38 -0
- data/spec/producer_spec.rb +71 -0
- data/spec/spec_helper.rb +18 -0
- metadata +107 -0
@@ -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
|
data/spec/kafka_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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 Kafka do
|
18
|
+
before(:each) do
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,227 @@
|
|
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 Message do
|
18
|
+
|
19
|
+
def pack_v1_message bytes, attributes
|
20
|
+
[6 + bytes.length, 1, attributes, Zlib.crc32(bytes), bytes].pack "NCCNa*"
|
21
|
+
end
|
22
|
+
|
23
|
+
before(:each) do
|
24
|
+
@message = Message.new
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Kafka Message" do
|
28
|
+
it "should have a default magic number" do
|
29
|
+
Message::MAGIC_IDENTIFIER_DEFAULT.should eql(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have a magic field, a checksum and a payload" do
|
33
|
+
[:magic, :checksum, :payload].each do |field|
|
34
|
+
@message.should respond_to(field.to_sym)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should set a default value of zero" do
|
39
|
+
@message.magic.should eql(Kafka::Message::MAGIC_IDENTIFIER_DEFAULT)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should allow to set a custom magic number" do
|
43
|
+
@message = Message.new("ale", 1)
|
44
|
+
@message.magic.should eql(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should have an empty payload by default" do
|
48
|
+
@message.payload.should == ""
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should calculate the checksum (crc32 of a given message)" do
|
52
|
+
@message.payload = "ale"
|
53
|
+
@message.calculate_checksum.should eql(1120192889)
|
54
|
+
@message.payload = "alejandro"
|
55
|
+
@message.calculate_checksum.should eql(2865078607)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should say if the message is valid using the crc32 signature" do
|
59
|
+
@message.payload = "alejandro"
|
60
|
+
@message.checksum = 2865078607
|
61
|
+
@message.valid?.should eql(true)
|
62
|
+
@message.checksum = 0
|
63
|
+
@message.valid?.should eql(false)
|
64
|
+
@message = Message.new("alejandro", 0, 66666666) # 66666666 is a funny checksum
|
65
|
+
@message.valid?.should eql(false)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "parsing" do
|
70
|
+
it "should parse a version-0 message from bytes" do
|
71
|
+
bytes = [8, 0, 1120192889, 'ale'].pack('NCNa*')
|
72
|
+
message = Kafka::Message.parse_from(bytes).messages.first
|
73
|
+
message.valid?.should eql(true)
|
74
|
+
message.magic.should eql(0)
|
75
|
+
message.checksum.should eql(1120192889)
|
76
|
+
message.payload.should eql("ale")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should parse a version-1 message from bytes" do
|
80
|
+
bytes = [12, 1, 0, 755095536, 'martin'].pack('NCCNa*')
|
81
|
+
message = Kafka::Message.parse_from(bytes).messages.first
|
82
|
+
message.should be_valid
|
83
|
+
message.magic.should == 1
|
84
|
+
message.checksum.should == 755095536
|
85
|
+
message.payload.should == 'martin'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should raise an error if the magic number is not recognised" do
|
89
|
+
bytes = [12, 2, 0, 755095536, 'martin'].pack('NCCNa*') # 2 = some future format that's not yet invented
|
90
|
+
lambda {
|
91
|
+
Kafka::Message.parse_from(bytes)
|
92
|
+
}.should raise_error(RuntimeError, /Unsupported Kafka message version/)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should skip an incomplete message at the end of the response" do
|
96
|
+
bytes = [8, 0, 1120192889, 'ale'].pack('NCNa*')
|
97
|
+
bytes += [8].pack('N') # incomplete message (only length, rest is truncated)
|
98
|
+
message_set = Message.parse_from(bytes)
|
99
|
+
message_set.messages.size.should == 1
|
100
|
+
message_set.size.should == 12 # bytes consumed
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should skip an incomplete message at the end of the response which has the same length as an empty message" do
|
104
|
+
bytes = [8, 0, 1120192889, 'ale'].pack('NCNa*')
|
105
|
+
bytes += [8, 0, 1120192889].pack('NCN') # incomplete message (payload is missing)
|
106
|
+
message_set = Message.parse_from(bytes)
|
107
|
+
message_set.messages.size.should == 1
|
108
|
+
message_set.size.should == 12 # bytes consumed
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should read empty messages correctly" do
|
112
|
+
# empty message
|
113
|
+
bytes = [5, 0, 0, ''].pack('NCNa*')
|
114
|
+
messages = Message.parse_from(bytes).messages
|
115
|
+
messages.size.should == 1
|
116
|
+
messages.first.payload.should == ''
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should parse a gzip-compressed message" do
|
120
|
+
compressed = 'H4sIAG0LI1AAA2NgYBBkZBB/9XN7YlJRYnJiCogCAH9lueQVAAAA'.unpack('m*').shift
|
121
|
+
bytes = [45, 1, 1, 1303540914, compressed].pack('NCCNa*')
|
122
|
+
message = Message.parse_from(bytes).messages.first
|
123
|
+
message.should be_valid
|
124
|
+
message.payload.should == 'abracadabra'
|
125
|
+
end
|
126
|
+
|
127
|
+
if Object.const_defined? "Snappy"
|
128
|
+
it "should parse a snappy-compressed message" do
|
129
|
+
cleartext = "abracadabra"
|
130
|
+
bytes = pack_v1_message cleartext, 0
|
131
|
+
compressed = Snappy.deflate(bytes)
|
132
|
+
bytes = pack_v1_message compressed, 2
|
133
|
+
message = Message.parse_from(bytes).messages.first
|
134
|
+
message.should be_valid
|
135
|
+
message.payload.should == cleartext
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should recursively parse nested snappy compressed messages" do
|
139
|
+
uncompressed = pack_v1_message('abracadabra', 0)
|
140
|
+
uncompressed << pack_v1_message('foobar', 0)
|
141
|
+
compressed = pack_v1_message(Snappy.deflate(uncompressed), 2)
|
142
|
+
messages = Message.parse_from(compressed).messages
|
143
|
+
messages.map(&:payload).should == ['abracadabra', 'foobar']
|
144
|
+
messages.map(&:valid?).should == [true, true]
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should support a mixture of snappy compressed and uncompressed messages" do
|
148
|
+
bytes = pack_v1_message(Snappy.deflate(pack_v1_message("compressed", 0)), 2)
|
149
|
+
bytes << pack_v1_message('uncompressed', 0)
|
150
|
+
messages = Message.parse_from(bytes).messages
|
151
|
+
messages.map(&:payload).should == ["compressed", "uncompressed"]
|
152
|
+
messages.map(&:valid?).should == [true, true]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should recursively parse nested gzip compressed messages" do
|
157
|
+
uncompressed = [17, 1, 0, 401275319, 'abracadabra'].pack('NCCNa*')
|
158
|
+
uncompressed << [12, 1, 0, 2666930069, 'foobar'].pack('NCCNa*')
|
159
|
+
compressed_io = StringIO.new('')
|
160
|
+
Zlib::GzipWriter.new(compressed_io).tap{|gzip| gzip << uncompressed; gzip.close }
|
161
|
+
compressed = compressed_io.string
|
162
|
+
bytes = [compressed.size + 6, 1, 1, Zlib.crc32(compressed), compressed].pack('NCCNa*')
|
163
|
+
messages = Message.parse_from(bytes).messages
|
164
|
+
messages.map(&:payload).should == ['abracadabra', 'foobar']
|
165
|
+
messages.map(&:valid?).should == [true, true]
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should support a mixture of gzip compressed and uncompressed messages" do
|
169
|
+
compressed = 'H4sIAG0LI1AAA2NgYBBkZBB/9XN7YlJRYnJiCogCAH9lueQVAAAA'.unpack('m*').shift
|
170
|
+
bytes = [45, 1, 1, 1303540914, compressed].pack('NCCNa*')
|
171
|
+
bytes << [11, 1, 0, 907060870, 'hello'].pack('NCCNa*')
|
172
|
+
messages = Message.parse_from(bytes).messages
|
173
|
+
messages.map(&:payload).should == ['abracadabra', 'hello']
|
174
|
+
messages.map(&:valid?).should == [true, true]
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should raise an error if the compression codec is not supported" do
|
178
|
+
bytes = [6, 1, 3, 0, ''].pack('NCCNa*') # 3 = some unknown future compression codec
|
179
|
+
lambda {
|
180
|
+
Kafka::Message.parse_from(bytes)
|
181
|
+
}.should raise_error(RuntimeError, /Unsupported Kafka compression codec/)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "#ensure_snappy!" do
|
186
|
+
let(:message) { Kafka::Message.new }
|
187
|
+
before { Kafka::Message.instance_variable_set :@snappy, nil }
|
188
|
+
|
189
|
+
subject { message.ensure_snappy! { 42 } }
|
190
|
+
|
191
|
+
if Object.const_defined? "Snappy"
|
192
|
+
context "when snappy is available" do
|
193
|
+
before { Object.stub! :const_defined? => true }
|
194
|
+
it { should == 42 }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "when snappy is not available" do
|
199
|
+
before { Object.stub! :const_defined? => false }
|
200
|
+
|
201
|
+
it "raises an error" do
|
202
|
+
expect { message.ensure_snappy! { 42 } }.to raise_error
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe ".ensure_snappy!" do
|
208
|
+
before { Kafka::Message.instance_variable_set :@snappy, nil }
|
209
|
+
|
210
|
+
subject { Kafka::Message.ensure_snappy! { 42 } }
|
211
|
+
|
212
|
+
if Object.const_defined? "Snappy"
|
213
|
+
context "when snappy is available" do
|
214
|
+
before { Object.stub! :const_defined? => true }
|
215
|
+
it { should == 42 }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context "when snappy is not available" do
|
220
|
+
before { Object.stub! :const_defined? => false }
|
221
|
+
|
222
|
+
it "raises an error" do
|
223
|
+
expect { Kafka::Message.ensure_snappy! { 42 } }.to raise_error
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|