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,209 @@
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
+ module Kafka
16
+
17
+ # A message. The format of a message is as follows:
18
+ #
19
+ # 4 byte big-endian int: length of message in bytes (including the rest of
20
+ # the header, but excluding the length field itself)
21
+ # 1 byte: "magic" identifier (format version number)
22
+ #
23
+ # If the magic byte == 0, there is one more header field:
24
+ #
25
+ # 4 byte big-endian int: CRC32 checksum of the payload
26
+ #
27
+ # If the magic byte == 1, there are two more header fields:
28
+ #
29
+ # 1 byte: "attributes" (flags for compression, codec etc)
30
+ # 4 byte big-endian int: CRC32 checksum of the payload
31
+ #
32
+ # All following bytes are the payload.
33
+ class Message
34
+
35
+ MAGIC_IDENTIFIER_DEFAULT = 0
36
+ MAGIC_IDENTIFIER_COMPRESSION = 1
37
+ NO_COMPRESSION = 0
38
+ GZIP_COMPRESSION = 1
39
+ SNAPPY_COMPRESSION = 2
40
+ BASIC_MESSAGE_HEADER = 'NC'.freeze
41
+ VERSION_0_HEADER = 'N'.freeze
42
+ VERSION_1_HEADER = 'CN'.freeze
43
+ COMPRESSION_CODEC_MASK = 0x03
44
+
45
+ attr_accessor :magic, :checksum, :payload
46
+
47
+ def initialize(payload = nil, magic = MAGIC_IDENTIFIER_DEFAULT, checksum = nil)
48
+ self.magic = magic
49
+ self.payload = payload || ""
50
+ self.checksum = checksum || self.calculate_checksum
51
+ @compression = NO_COMPRESSION
52
+ end
53
+
54
+ def calculate_checksum
55
+ Zlib.crc32(self.payload)
56
+ end
57
+
58
+ def valid?
59
+ self.checksum == calculate_checksum
60
+ end
61
+
62
+ # Takes a byte string containing one or more messages; returns a MessageSet
63
+ # with the messages parsed from the string, and the number of bytes
64
+ # consumed from the string.
65
+ def self.parse_from(data)
66
+ messages = []
67
+ bytes_processed = 0
68
+
69
+ while bytes_processed <= data.length - 5 # 5 = size of BASIC_MESSAGE_HEADER
70
+ message_size, magic = data[bytes_processed, 5].unpack(BASIC_MESSAGE_HEADER)
71
+ break if bytes_processed + message_size + 4 > data.length # message is truncated
72
+
73
+ case magic
74
+ when MAGIC_IDENTIFIER_DEFAULT
75
+ # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ...
76
+ # | | | |
77
+ # | message_size |magic| checksum | payload ...
78
+ payload_size = message_size - 5 # 5 = sizeof(magic) + sizeof(checksum)
79
+ checksum = data[bytes_processed + 5, 4].unpack(VERSION_0_HEADER).shift
80
+ payload = data[bytes_processed + 9, payload_size]
81
+ messages << Kafka::Message.new(payload, magic, checksum)
82
+
83
+ when MAGIC_IDENTIFIER_COMPRESSION
84
+ # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 ...
85
+ # | | | | |
86
+ # | size |magic|attrs| checksum | payload ...
87
+ payload_size = message_size - 6 # 6 = sizeof(magic) + sizeof(attrs) + sizeof(checksum)
88
+ attributes, checksum = data[bytes_processed + 5, 5].unpack(VERSION_1_HEADER)
89
+ payload = data[bytes_processed + 10, payload_size]
90
+
91
+ case attributes & COMPRESSION_CODEC_MASK
92
+ when NO_COMPRESSION # a single uncompressed message
93
+ messages << Kafka::Message.new(payload, magic, checksum)
94
+ when GZIP_COMPRESSION # a gzip-compressed message set -- parse recursively
95
+ uncompressed = Zlib::GzipReader.new(StringIO.new(payload)).read
96
+ message_set = parse_from(uncompressed)
97
+ raise 'malformed compressed message' if message_set.size != uncompressed.size
98
+ messages.concat(message_set.messages)
99
+ when SNAPPY_COMPRESSION # a snappy-compresses message set -- parse recursively
100
+ ensure_snappy! do
101
+ uncompressed = Snappy::Reader.new(StringIO.new(payload)).read
102
+ message_set = parse_from(uncompressed)
103
+ raise 'malformed compressed message' if message_set.size != uncompressed.size
104
+ messages.concat(message_set.messages)
105
+ end
106
+ else
107
+ # https://cwiki.apache.org/confluence/display/KAFKA/Compression
108
+ raise "Unsupported Kafka compression codec: #{attributes & COMPRESSION_CODEC_MASK}"
109
+ end
110
+
111
+ else
112
+ raise "Unsupported Kafka message version: magic number #{magic}"
113
+ end
114
+
115
+ bytes_processed += message_size + 4 # 4 = sizeof(message_size)
116
+ end
117
+
118
+ MessageSet.new(bytes_processed, messages)
119
+ end
120
+
121
+ def encode(compression = NO_COMPRESSION)
122
+ @compression = compression
123
+
124
+ self.payload = asciify_payload
125
+ self.payload = compress_payload if compression?
126
+
127
+ data = magic_and_compression + [calculate_checksum].pack("N") + payload
128
+ [data.length].pack("N") + data
129
+ end
130
+
131
+
132
+ # Encapsulates a list of Kafka messages (as Kafka::Message objects in the
133
+ # +messages+ attribute) and their total serialized size in bytes (the +size+
134
+ # attribute).
135
+ class MessageSet < Struct.new(:size, :messages); end
136
+
137
+ def self.ensure_snappy!
138
+ if Object.const_defined? "Snappy"
139
+ yield
140
+ else
141
+ fail "Snappy not available!"
142
+ end
143
+ end
144
+
145
+ def ensure_snappy! &block
146
+ self.class.ensure_snappy! &block
147
+ end
148
+
149
+ private
150
+
151
+ attr_reader :compression
152
+
153
+ def compression?
154
+ compression != NO_COMPRESSION
155
+ end
156
+
157
+ def magic_and_compression
158
+ if compression?
159
+ [MAGIC_IDENTIFIER_COMPRESSION, compression].pack("CC")
160
+ else
161
+ [MAGIC_IDENTIFIER_DEFAULT].pack("C")
162
+ end
163
+ end
164
+
165
+ def asciify_payload
166
+ if RUBY_VERSION[0, 3] == "1.8"
167
+ payload
168
+ else
169
+ payload.to_s.force_encoding(Encoding::ASCII_8BIT)
170
+ end
171
+ end
172
+
173
+ def compress_payload
174
+ case compression
175
+ when GZIP_COMPRESSION
176
+ gzip
177
+ when SNAPPY_COMPRESSION
178
+ snappy
179
+ end
180
+ end
181
+
182
+ def gzip
183
+ with_buffer do |buffer|
184
+ gz = Zlib::GzipWriter.new buffer, nil, nil
185
+ gz.write payload
186
+ gz.close
187
+ end
188
+ end
189
+
190
+ def snappy
191
+ ensure_snappy! do
192
+ with_buffer do |buffer|
193
+ Snappy::Writer.new buffer do |w|
194
+ w << payload
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def with_buffer
201
+ buffer = StringIO.new
202
+ buffer.set_encoding Encoding::ASCII_8BIT unless RUBY_VERSION =~ /^1\.8/
203
+ yield buffer if block_given?
204
+ buffer.rewind
205
+ buffer.string
206
+ end
207
+ end
208
+ end
209
+
@@ -0,0 +1,35 @@
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
+ module Kafka
16
+ class MultiProducer
17
+ include Kafka::IO
18
+
19
+ def initialize(options={})
20
+ self.host = options[:host] || HOST
21
+ self.port = options[:port] || PORT
22
+ self.compression = options[:compression] || Message::NO_COMPRESSION
23
+ self.connect(self.host, self.port)
24
+ end
25
+
26
+ def push(topic, messages, options={})
27
+ partition = options[:partition] || 0
28
+ self.write(Encoder.produce(topic, partition, messages, compression))
29
+ end
30
+
31
+ def multi_push(producer_requests)
32
+ self.write(Encoder.multiproduce(producer_requests, compression))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
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
+ module Kafka
16
+ class Producer
17
+
18
+ include Kafka::IO
19
+
20
+ attr_accessor :topic, :partition
21
+
22
+ def initialize(options = {})
23
+ self.topic = options[:topic] || "test"
24
+ self.partition = options[:partition] || 0
25
+ self.host = options[:host] || HOST
26
+ self.port = options[:port] || PORT
27
+ self.compression = options[:compression] || Message::NO_COMPRESSION
28
+ self.connect(self.host, self.port)
29
+ end
30
+
31
+ def push(messages)
32
+ self.write(Encoder.produce(self.topic, self.partition, messages, compression))
33
+ end
34
+
35
+ def batch(&block)
36
+ batch = Kafka::Batch.new
37
+ block.call( batch )
38
+ push(batch.messages)
39
+ batch.messages.clear
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
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
+
16
+ module Kafka
17
+ class ProducerRequest
18
+ attr_accessor :topic, :messages, :partition
19
+
20
+ def initialize(topic, messages, options={})
21
+ self.topic = topic
22
+ self.partition = options[:partition] || 0
23
+ self.messages = Array(messages)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
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
+ module Kafka
16
+ module RequestType
17
+ PRODUCE = 0
18
+ FETCH = 1
19
+ MULTIFETCH = 2
20
+ MULTIPRODUCE = 3
21
+ OFFSETS = 4
22
+ end
23
+ end
@@ -0,0 +1,16 @@
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
+
16
+ require File.join(File.dirname(__FILE__), "kafka")
@@ -0,0 +1,16 @@
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
+
16
+ require File.join(File.dirname(__FILE__), "kafka")
@@ -0,0 +1,35 @@
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 Batch do
18
+
19
+ before(:each) do
20
+ @batch = Batch.new
21
+ end
22
+
23
+ describe "batch messages" do
24
+ it "holds all messages to be sent" do
25
+ @batch.should respond_to(:messages)
26
+ @batch.messages.class.should eql(Array)
27
+ end
28
+
29
+ it "supports queueing/adding messages to be send" do
30
+ @batch.messages << mock(Kafka::Message.new("one"))
31
+ @batch.messages << mock(Kafka::Message.new("two"))
32
+ @batch.messages.length.should eql(2)
33
+ end
34
+ end
35
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,133 @@
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
+ require 'kafka/cli'
17
+
18
+ describe CLI do
19
+
20
+ before(:each) do
21
+ CLI.instance_variable_set("@config", {})
22
+ CLI.stub(:puts)
23
+ end
24
+
25
+ describe "should read from env" do
26
+ describe "kafka host" do
27
+ it "should read KAFKA_HOST from env" do
28
+ CLI.read_env("KAFKA_HOST" => "google.com")
29
+ CLI.config[:host].should == "google.com"
30
+ end
31
+
32
+ it "kafka port" do
33
+ CLI.read_env("KAFKA_PORT" => "1234")
34
+ CLI.config[:port].should == 1234
35
+ end
36
+
37
+ it "kafka topic" do
38
+ CLI.read_env("KAFKA_TOPIC" => "news")
39
+ CLI.config[:topic].should == "news"
40
+ end
41
+
42
+ it "kafka compression" do
43
+ CLI.read_env("KAFKA_COMPRESSION" => "no")
44
+ CLI.config[:compression].should == Message::NO_COMPRESSION
45
+
46
+ CLI.read_env("KAFKA_COMPRESSION" => "gzip")
47
+ CLI.config[:compression].should == Message::GZIP_COMPRESSION
48
+
49
+ CLI.read_env("KAFKA_COMPRESSION" => "snappy")
50
+ CLI.config[:compression].should == Message::SNAPPY_COMPRESSION
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "should read from command line" do
56
+ it "kafka host" do
57
+ CLI.parse_args(%w(--host google.com))
58
+ CLI.config[:host].should == "google.com"
59
+
60
+ CLI.parse_args(%w(-h google.com))
61
+ CLI.config[:host].should == "google.com"
62
+ end
63
+
64
+ it "kafka port" do
65
+ CLI.parse_args(%w(--port 1234))
66
+ CLI.config[:port].should == 1234
67
+
68
+ CLI.parse_args(%w(-p 1234))
69
+ CLI.config[:port].should == 1234
70
+ end
71
+
72
+ it "kafka topic" do
73
+ CLI.parse_args(%w(--topic news))
74
+ CLI.config[:topic].should == "news"
75
+
76
+ CLI.parse_args(%w(-t news))
77
+ CLI.config[:topic].should == "news"
78
+ end
79
+
80
+ it "kafka compression" do
81
+ CLI.stub(:publish? => true)
82
+
83
+ CLI.parse_args(%w(--compression no))
84
+ CLI.config[:compression].should == Message::NO_COMPRESSION
85
+ CLI.parse_args(%w(-c no))
86
+ CLI.config[:compression].should == Message::NO_COMPRESSION
87
+
88
+ CLI.parse_args(%w(--compression gzip))
89
+ CLI.config[:compression].should == Message::GZIP_COMPRESSION
90
+ CLI.parse_args(%w(-c gzip))
91
+ CLI.config[:compression].should == Message::GZIP_COMPRESSION
92
+
93
+ CLI.parse_args(%w(--compression snappy))
94
+ CLI.config[:compression].should == Message::SNAPPY_COMPRESSION
95
+ CLI.parse_args(%w(-c snappy))
96
+ CLI.config[:compression].should == Message::SNAPPY_COMPRESSION
97
+ end
98
+
99
+ it "message" do
100
+ CLI.stub(:publish? => true)
101
+ CLI.parse_args(%w(--message YEAH))
102
+ CLI.config[:message].should == "YEAH"
103
+
104
+ CLI.parse_args(%w(-m YEAH))
105
+ CLI.config[:message].should == "YEAH"
106
+ end
107
+
108
+ end
109
+
110
+ describe "config validation" do
111
+ it "should assign a default port" do
112
+ CLI.stub(:exit)
113
+ CLI.stub(:puts)
114
+ CLI.validate_config
115
+ CLI.config[:port].should == Kafka::IO::PORT
116
+ end
117
+ end
118
+
119
+ it "should assign a default host" do
120
+ CLI.stub(:exit)
121
+ CLI.validate_config
122
+ CLI.config[:host].should == Kafka::IO::HOST
123
+ end
124
+
125
+
126
+ it "read compression method" do
127
+ CLI.string_to_compression("no").should == Message::NO_COMPRESSION
128
+ CLI.string_to_compression("gzip").should == Message::GZIP_COMPRESSION
129
+ CLI.string_to_compression("snappy").should == Message::SNAPPY_COMPRESSION
130
+ lambda { CLI.push(:string_to_compression,nil) }.should raise_error
131
+ end
132
+
133
+ end