kafka-rb 0.0.1

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 ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Alejandro Crosa
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # kafka-rb
2
+ kafka-rb allows you to produce messages to the Kafka distributed publish/subscribe messaging service.
3
+
4
+ ## Requirements
5
+ You need to have access to your Kafka instance and be able to connect through TCP. You can obtain a copy and instructions on how to setup kafka at https://github.com/kafka-dev/kafka
6
+
7
+ ## Installation
8
+ sudo gem install kafka-rb
9
+
10
+ (the code works fine with JRuby, Ruby 1.8x and Ruby 1.9.x)
11
+
12
+ ## Usage
13
+
14
+ ### Sending a simple message
15
+
16
+ require 'kafka-rb'
17
+
18
+ producer = Kafka::Producer.new
19
+
20
+ message = Kafka::Message.new("some random message content")
21
+
22
+ producer.send(message)
23
+
24
+ ### sending a sequence of messages
25
+
26
+ require 'kafka-rb'
27
+
28
+ producer = Kafka::Producer.new
29
+
30
+ message1 = Kafka::Message.new("some random message content")
31
+
32
+ message2 = Kafka::Message.new("some more content")
33
+
34
+ producer.send([message1, message2])
35
+
36
+ ### batching a bunch of messages using the block syntax
37
+
38
+ require 'kafka-rb'
39
+
40
+ producer = Kafka::Producer.new
41
+
42
+ producer.batch do |messages|
43
+
44
+ puts "Batching a send of multiple messages.."
45
+
46
+ messages << Kafka::Message.new("first message to send")
47
+
48
+ messages << Kafka::Message.new("second message to send")
49
+
50
+ end
51
+
52
+ * they will be sent all at once, after the block execution
data/Rakefile ADDED
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = 'kafka-rb'
8
+ GEM_NAME = 'Kafka Client Producer'
9
+ GEM_VERSION = '0.0.1'
10
+ AUTHORS = ['Alejandro Crosa']
11
+ EMAIL = "alejandrocrosa@gmail.com"
12
+ HOMEPAGE = "http://github.com/acrosa/kafka-rb"
13
+ SUMMARY = "A Ruby client for the Kafka distributed publish/subscribe messaging service"
14
+ DESCRIPTION = "kafka-rb allows you to produce messages to the Kafka distributed publish/subscribe messaging service."
15
+
16
+ spec = Gem::Specification.new do |s|
17
+ s.name = GEM
18
+ s.version = GEM_VERSION
19
+ s.platform = Gem::Platform::RUBY
20
+ s.has_rdoc = true
21
+ s.extra_rdoc_files = ["LICENSE"]
22
+ s.summary = SUMMARY
23
+ s.description = DESCRIPTION
24
+ s.authors = AUTHORS
25
+ s.email = EMAIL
26
+ s.homepage = HOMEPAGE
27
+ s.add_development_dependency "rspec"
28
+ s.require_path = 'lib'
29
+ s.autorequire = GEM
30
+ s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
31
+ end
32
+
33
+ task :default => :spec
34
+
35
+ desc "Run specs"
36
+ Spec::Rake::SpecTask.new do |t|
37
+ t.spec_files = FileList['spec/**/*_spec.rb']
38
+ t.spec_opts = %w(-fs --color)
39
+ end
40
+
41
+ Rake::GemPackageTask.new(spec) do |pkg|
42
+ pkg.gem_spec = spec
43
+ end
44
+
45
+ desc "install the gem locally"
46
+ task :install => [:package] do
47
+ sh %{sudo gem install pkg/#{GEM_NAME}-#{GEM_VERSION}}
48
+ end
49
+
50
+ desc "create a gemspec file"
51
+ task :make_spec do
52
+ File.open("#{GEM}.gemspec", "w") do |file|
53
+ file.puts spec.to_ruby
54
+ end
55
+ end
56
+
57
+ desc "Run all examples with RCov"
58
+ Spec::Rake::SpecTask.new(:rcov) do |t|
59
+ t.spec_files = FileList['spec/**/*_spec.rb']
60
+ t.rcov = true
61
+ end
data/lib/kafka/io.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Kafka
2
+ module IO
3
+ attr_accessor :socket, :host, :port
4
+
5
+ def connect(host, port)
6
+ raise ArgumentError, "No host or port specified" unless host && port
7
+ self.host = host
8
+ self.port = port
9
+ self.socket = TCPSocket.new(host, port)
10
+ end
11
+
12
+ def reconnect
13
+ self.disconnect
14
+ self.socket = self.connect(self.host, self.port)
15
+ end
16
+
17
+ def disconnect
18
+ self.socket.close rescue nil
19
+ self.socket = nil
20
+ end
21
+
22
+ def write(data)
23
+ self.reconnect unless self.socket
24
+ self.socket.write(data)
25
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
26
+ self.reconnect
27
+ self.socket.write(data) # retry
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module Kafka
2
+
3
+ # A message. The format of an N byte message is the following:
4
+ # 1 byte "magic" identifier to allow format changes
5
+ # 4 byte CRC32 of the payload
6
+ # N - 5 byte payload
7
+ class Message
8
+ MAGIC_IDENTIFIER_DEFAULT = 0
9
+ attr_accessor :magic, :checksum, :payload
10
+
11
+ def initialize(payload = nil, magic = MAGIC_IDENTIFIER_DEFAULT)
12
+ self.magic = magic
13
+ self.payload = payload
14
+ self.checksum = self.calculate_checksum
15
+ end
16
+
17
+ def calculate_checksum
18
+ Zlib.crc32(self.payload)
19
+ end
20
+
21
+ def valid?
22
+ self.checksum == Zlib.crc32(self.payload)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module Kafka
2
+ class Producer
3
+
4
+ include Kafka::IO
5
+
6
+ PRODUCE_REQUEST_ID = 0
7
+
8
+ attr_accessor :topic, :partition
9
+
10
+ def initialize(options = {})
11
+ self.topic = options[:topic] || "test"
12
+ self.partition = options[:partition] || 0
13
+ self.host = options[:host] || "localhost"
14
+ self.port = options[:port] || 9092
15
+ self.connect(self.host, self.port)
16
+ end
17
+
18
+ def encode(message)
19
+ [message.magic].pack("C") + [message.calculate_checksum].pack("N") + message.payload.to_s
20
+ end
21
+
22
+ def encode_request(topic, partition, messages)
23
+ message_set = Array(messages).collect { |message|
24
+ encoded_message = self.encode(message)
25
+ [encoded_message.length].pack("N") + encoded_message
26
+ }.join("")
27
+
28
+ request = [PRODUCE_REQUEST_ID].pack("n")
29
+ topic = [topic.length].pack("n") + topic
30
+ partition = [partition].pack("N")
31
+ messages = [message_set.length].pack("N") + message_set
32
+
33
+ data = request + topic + partition + messages
34
+
35
+ return [data.length].pack("N") + data
36
+ end
37
+
38
+ def send(messages)
39
+ self.write(self.encode_request(self.topic, self.partition, messages))
40
+ end
41
+
42
+ def batch(&block)
43
+ batch = Kafka::Batch.new
44
+ block.call( batch )
45
+ self.send(batch.messages)
46
+ batch.messages.clear
47
+ end
48
+ end
49
+ end
data/lib/kafka.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'socket'
2
+ require 'zlib'
3
+ require File.join(File.dirname(__FILE__), "kafka", "io")
4
+ require File.join(File.dirname(__FILE__), "kafka", "batch")
5
+ require File.join(File.dirname(__FILE__), "kafka", "message")
6
+ require File.join(File.dirname(__FILE__), "kafka", "producer")
7
+
8
+ module Kafka
9
+ end
data/lib/test.rb ADDED
@@ -0,0 +1,32 @@
1
+ $KCODE = 'UTF-8'
2
+
3
+ require 'zlib'
4
+
5
+ PRODUCE_REQUEST_ID = 0
6
+
7
+ def encode_message(message)
8
+ # <MAGIC_BYTE: char> <CRC32: int> <PAYLOAD: bytes>
9
+ data = [0].pack("C").to_s + [Zlib.crc32(message)].pack('N').to_s + message
10
+ # print ("CHECKSUM " + Zlib.crc32(message).to_s)
11
+ # print ("MES " + data.length.to_s)
12
+ return data
13
+ end
14
+ # encode_message("ale")
15
+
16
+ def encode_produce_request(topic, partition, messages)
17
+ encoded = messages.collect { |m| encode_message(m) }
18
+ message_set = encoded.collect { |e| puts "Message size #{e.length}"; [e.length].pack("N") + e }.join("")
19
+
20
+ puts "MESSAGE"
21
+ puts message_set.inspect
22
+
23
+ data = [PRODUCE_REQUEST_ID].pack("n") + \
24
+ [topic.length].pack("n") + topic.to_s + \
25
+ [partition].pack("N") + \
26
+ [message_set.length].pack("N") + message_set
27
+ puts "DATA " + message_set.length.to_s
28
+ return [data.length].pack("N") + data
29
+ end
30
+
31
+ socket = TCPSocket.open("localhost", 9092)
32
+ socket.write encode_produce_request("test", 0, ["ale"])
data/spec/io_spec.rb ADDED
@@ -0,0 +1,64 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class IOTest
4
+ include Kafka::IO
5
+ end
6
+
7
+ describe IO do
8
+
9
+ before(:each) do
10
+ @mocked_socket = mock(TCPSocket)
11
+ TCPSocket.stub!(:new).and_return(@mocked_socket) # don't use a real socket
12
+ @io = IOTest.new
13
+ @io.connect("somehost", 9093)
14
+ end
15
+
16
+ describe "default methods" do
17
+ it "has a socket, a host and a port" do
18
+ [:socket, :host, :port].each do |m|
19
+ @io.should respond_to(m.to_sym)
20
+ end
21
+ end
22
+
23
+ it "raises an exception if no host and port is specified" do
24
+ lambda {
25
+ io = IOTest.new
26
+ io.connect
27
+ }.should raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should remember the port and host on connect" do
31
+ @io.connect("somehost", 9093)
32
+ @io.host.should eql("somehost")
33
+ @io.port.should eql(9093)
34
+ end
35
+
36
+ it "should write to a socket" do
37
+ data = "some data"
38
+ @mocked_socket.should_receive(:write).with(data).and_return(9)
39
+ @io.write(data).should eql(9)
40
+ end
41
+
42
+ it "should disconnect" do
43
+ @io.should respond_to(:disconnect)
44
+ @mocked_socket.should_receive(:close).and_return(nil)
45
+ @io.disconnect
46
+ end
47
+
48
+ it "should reconnect" do
49
+ @mocked_socket.should_receive(:close)
50
+ @io.should_receive(:connect)
51
+ @io.reconnect
52
+ end
53
+
54
+ it "should reconnect on a broken pipe error" do
55
+ [Errno::ECONNABORTED, Errno::EPIPE, Errno::ECONNRESET].each do |error|
56
+ @mocked_socket.should_receive(:write).exactly(:twice).and_raise(error)
57
+ @mocked_socket.should_receive(:close).exactly(:once).and_return(nil)
58
+ lambda {
59
+ @io.write("some data to send")
60
+ }.should raise_error(error)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Kafka do
4
+
5
+ before(:each) do
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Message do
4
+
5
+ before(:each) do
6
+ @message = Message.new
7
+ end
8
+
9
+ describe "Kafka Message" do
10
+ it "should have a default magic number" do
11
+ Message::MAGIC_IDENTIFIER_DEFAULT.should eql(0)
12
+ end
13
+
14
+ it "should have a magic field, a checksum and a payload" do
15
+ [:magic, :checksum, :payload].each do |field|
16
+ @message.should respond_to(field.to_sym)
17
+ end
18
+ end
19
+
20
+ it "should set a default value of zero" do
21
+ @message.magic.should eql(Kafka::Message::MAGIC_IDENTIFIER_DEFAULT)
22
+ end
23
+
24
+ it "should allow to set a custom magic number" do
25
+ @message = Message.new("ale", 1)
26
+ @message.magic.should eql(1)
27
+ end
28
+
29
+ it "should calculate the checksum (crc32 of a given message)" do
30
+ @message.payload = "ale"
31
+ @message.calculate_checksum.should eql(1120192889)
32
+ @message.payload = "alejandro"
33
+ @message.calculate_checksum.should eql(2865078607)
34
+ end
35
+
36
+ it "should say if the message is valid using the crc32 signature" do
37
+ @message.payload = "alejandro"
38
+ @message.checksum = 2865078607
39
+ @message.valid?.should eql(true)
40
+ @message.checksum = 0
41
+ @message.valid?.should eql(false)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,95 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Producer do
4
+
5
+ before(:each) do
6
+ @mocked_socket = mock(TCPSocket)
7
+ TCPSocket.stub!(:new).and_return(@mocked_socket) # don't use a real socket
8
+ @producer = Producer.new
9
+ end
10
+
11
+ describe "Kafka Producer" do
12
+ it "should have a PRODUCE_REQUEST_ID" do
13
+ Producer::PRODUCE_REQUEST_ID.should eql(0)
14
+ end
15
+
16
+ it "should have a topic and a partition" do
17
+ @producer.should respond_to(:topic)
18
+ @producer.should respond_to(:partition)
19
+ end
20
+
21
+ it "should set a topic and partition on initialize" do
22
+ @producer = Producer.new({ :host => "localhost", :port => 9092, :topic => "testing" })
23
+ @producer.topic.should eql("testing")
24
+ @producer.partition.should eql(0)
25
+ @producer = Producer.new({ :topic => "testing", :partition => 3 })
26
+ @producer.partition.should eql(3)
27
+ end
28
+
29
+ it "should set default host and port if none is specified" do
30
+ @producer = Producer.new
31
+ @producer.host.should eql("localhost")
32
+ @producer.port.should eql(9092)
33
+ end
34
+
35
+ describe "Message Encoding" do
36
+ it "should encode a message" do
37
+ message = Kafka::Message.new("alejandro")
38
+ full_message = [message.magic].pack("C") + [message.calculate_checksum].pack("N") + message.payload
39
+ @producer.encode(message).should eql(full_message)
40
+ end
41
+
42
+ it "should encode an empty message" do
43
+ message = Kafka::Message.new()
44
+ full_message = [message.magic].pack("C") + [message.calculate_checksum].pack("N") + message.payload.to_s
45
+ @producer.encode(message).should eql(full_message)
46
+ end
47
+ end
48
+
49
+ describe "Request Encoding" do
50
+ it "should binary encode an empty request" do
51
+ bytes = @producer.encode_request("test", 0, [])
52
+ bytes.length.should eql(20)
53
+ bytes.should eql("\000\000\000\020\000\000\000\004test\000\000\000\000\000\000\000\000")
54
+ end
55
+
56
+ it "should binary encode a request with a message, using a specific wire format" do
57
+ message = Kafka::Message.new("ale")
58
+ bytes = @producer.encode_request("test", 3, message)
59
+ data_size = bytes[0, 4].unpack("N").shift
60
+ request_id = bytes[4, 2].unpack("n").shift
61
+ topic_length = bytes[6, 2].unpack("n").shift
62
+ topic = bytes[8, 4]
63
+ partition = bytes[12, 4].unpack("N").shift
64
+ messages_length = bytes[16, 4].unpack("N").shift
65
+ messages = bytes[20, messages_length]
66
+
67
+ bytes.length.should eql(32)
68
+ data_size.should eql(28)
69
+ request_id.should eql(0)
70
+ topic_length.should eql(4)
71
+ topic.should eql("test")
72
+ partition.should eql(3)
73
+ messages_length.should eql(12)
74
+ end
75
+ end
76
+ end
77
+
78
+ it "should send messages" do
79
+ @producer.should_receive(:write).and_return(32)
80
+ message = Kafka::Message.new("ale")
81
+ @producer.send(message).should eql(32)
82
+ end
83
+
84
+ describe "Message Batching" do
85
+ it "should batch messages and send them at once" do
86
+ message1 = Kafka::Message.new("one")
87
+ message2 = Kafka::Message.new("two")
88
+ @producer.should_receive(:send).with([message1, message2]).exactly(:once).and_return(nil)
89
+ @producer.batch do |messages|
90
+ messages << message1
91
+ messages << message2
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'kafka'
3
+
4
+ include Kafka
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kafka-rb
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Alejandro Crosa
14
+ autorequire: kafka-rb
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-07 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: kafka-rb allows you to produce messages to the Kafka distributed publish/subscribe messaging service.
36
+ email: alejandrocrosa@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ files:
44
+ - LICENSE
45
+ - README.md
46
+ - Rakefile
47
+ - lib/kafka/io.rb
48
+ - lib/kafka/message.rb
49
+ - lib/kafka/producer.rb
50
+ - lib/kafka.rb
51
+ - lib/test.rb
52
+ - spec/io_spec.rb
53
+ - spec/kafka_spec.rb
54
+ - spec/message_spec.rb
55
+ - spec/producer_spec.rb
56
+ - spec/spec_helper.rb
57
+ has_rdoc: true
58
+ homepage: http://github.com/acrosa/kafka-rb
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.3.7
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: A Ruby client for the Kafka distributed publish/subscribe messaging service
91
+ test_files: []
92
+