leffen-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.
- 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
|