kafka-rb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+