em-kafka 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.
- checksums.yaml +15 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +22 -0
- data/README.md +42 -0
- data/Rakefile +4 -0
- data/bin/consume +14 -0
- data/bin/produce +17 -0
- data/em-kafka.gemspec +21 -0
- data/lib/em-kafka/client.rb +32 -0
- data/lib/em-kafka/connection.rb +29 -0
- data/lib/em-kafka/consumer.rb +49 -0
- data/lib/em-kafka/consumer_request.rb +23 -0
- data/lib/em-kafka/event_emitter.rb +29 -0
- data/lib/em-kafka/message.rb +34 -0
- data/lib/em-kafka/parser.rb +66 -0
- data/lib/em-kafka/producer.rb +26 -0
- data/lib/em-kafka/producer_request.rb +38 -0
- data/lib/em-kafka/version.rb +5 -0
- data/lib/em-kafka.rb +48 -0
- data/spec/em-kafka/consumer_request_spec.rb +32 -0
- data/spec/em-kafka/consumer_spec.rb +24 -0
- data/spec/em-kafka/message_spec.rb +30 -0
- data/spec/em-kafka/parser_spec.rb +86 -0
- data/spec/em-kafka/producer_request_spec.rb +41 -0
- data/spec/em-kafka/producer_spec.rb +34 -0
- data/spec/em-kafka_spec.rb +11 -0
- data/spec/spec_helper.rb +10 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NzM5ZGZjMjg0YzI4MTg4NGZiYzk2NTM2Y2Q3ODI5ZTFmYzQwMmMzMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZjA2NDFhYWU1NDY0NTIzNmQ2MWY2YjJmZDI2YmViODAyZjNiYjBmMg==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MWI4N2FiODA4ZjZhZDQ2YmJhZWUzZDY2NjA1YjY0NTRjN2Y2YjBiZGFiNGRk
|
10
|
+
ZDFmZDU2NzdlY2U0YzFmNWEzYjlhYjI0MTVkODE2YWQzOWIxOGZjN2U3NDFj
|
11
|
+
ZmM5ODMxZDE4OTU4ZmZmODBkODU1ZjhmNWI0ZDFjZmQ5YTgzMmU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZjQ1ZDlmNmI5MDljNTUwNGI5M2JhYzJiNmIzMjJkMGMyNTg1ODNiMDZjMjBj
|
14
|
+
MmQ1M2M3NjM1MjNlYmNhODU3NzhmMjc2YTUzNTg0NGU4NjA0ZjQ2ZWJmYTI3
|
15
|
+
NjhhNGM2OThjOGYxZWFhMDg5NWNmMGI4NzZiNjUwZWI2ODY2ZmE=
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.rvmrc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
em-kafka (0.0.1)
|
5
|
+
eventmachine (>= 1.0.0.beta.4)
|
6
|
+
yajl-ruby (>= 0.8.2)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
eventmachine (1.0.0.beta.4)
|
13
|
+
rspec (2.6.0)
|
14
|
+
rspec-core (~> 2.6.0)
|
15
|
+
rspec-expectations (~> 2.6.0)
|
16
|
+
rspec-mocks (~> 2.6.0)
|
17
|
+
rspec-core (2.6.4)
|
18
|
+
rspec-expectations (2.6.0)
|
19
|
+
diff-lcs (~> 1.1.2)
|
20
|
+
rspec-mocks (2.6.0)
|
21
|
+
yajl-ruby (1.1.0)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
em-kafka!
|
28
|
+
rspec (~> 2.6.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT-License)
|
2
|
+
|
3
|
+
Copyright (c) 2011 GroupMe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# EM-Kafka
|
2
|
+
|
3
|
+
EventMachine driver for [Kafka](http://incubator.apache.org/kafka/index.html).
|
4
|
+
|
5
|
+
## Producer
|
6
|
+
|
7
|
+
When using Ruby objects, the payload is encoded to JSON
|
8
|
+
|
9
|
+
producer = EM::Kafka::Producer.new("kafka://topic@localhost:9092/0")
|
10
|
+
producer.deliver(:foo => "bar") # payload is {foo:"bar"}
|
11
|
+
|
12
|
+
## Consumer
|
13
|
+
|
14
|
+
consumer = EM::Kafka::Consumer.new("kafka://topic@localhost:9092/0")
|
15
|
+
consumer.consume do |message|
|
16
|
+
puts message.payload
|
17
|
+
end
|
18
|
+
|
19
|
+
## Messages
|
20
|
+
|
21
|
+
Messages are composed of:
|
22
|
+
|
23
|
+
* a payload
|
24
|
+
* a magic id (defaults to 0)
|
25
|
+
|
26
|
+
Change the magic id when the payload format changes:
|
27
|
+
|
28
|
+
EM::Kafka::Message.new("payload", 2)
|
29
|
+
|
30
|
+
Pass messages when you want to be specific:
|
31
|
+
|
32
|
+
message_1 = EM::Kafka::Message.new("payload_1", 2)
|
33
|
+
message_2 = EM::Kafka::Message.new("payload_2", 2)
|
34
|
+
producer.deliver([message_1, message_2])
|
35
|
+
|
36
|
+
|
37
|
+
## Credits
|
38
|
+
|
39
|
+
Heavily influenced by / borrowed from:
|
40
|
+
|
41
|
+
* kafka-rb (Alejandro Crosa)
|
42
|
+
* em-hiredis (Martyn Loughran)
|
data/Rakefile
ADDED
data/bin/consume
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative "../lib/em-kafka"
|
3
|
+
|
4
|
+
topic = ARGV[0] || "test"
|
5
|
+
|
6
|
+
EM.run do
|
7
|
+
trap("TERM") { EM.stop; exit; }
|
8
|
+
consumer = EM::Kafka::Consumer.new(:topic => topic)
|
9
|
+
puts "consuming topic #{consumer.topic}"
|
10
|
+
consumer.consume do |message|
|
11
|
+
puts "payload: #{message.payload}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
data/bin/produce
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative "../lib/em-kafka"
|
3
|
+
|
4
|
+
topic = ARGV[0] || "test"
|
5
|
+
puts "Producing topic '#{topic}'"
|
6
|
+
|
7
|
+
EM.run do
|
8
|
+
trap("TERM") { EM.stop; exit; }
|
9
|
+
producer = EM::Kafka::Producer.new(:topic => topic)
|
10
|
+
puts "topic is #{producer.topic}"
|
11
|
+
|
12
|
+
EM.add_periodic_timer(0.25) {
|
13
|
+
message = EM::Kafka::Message.new("hello-#{Time.now.to_i}")
|
14
|
+
producer.deliver(message)
|
15
|
+
puts "Sending #{message.payload}"
|
16
|
+
}
|
17
|
+
end
|
data/em-kafka.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-kafka/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "em-kafka"
|
7
|
+
s.version = EventMachine::Kafka::VERSION
|
8
|
+
s.authors = ["Brandon Keene"]
|
9
|
+
s.email = ["bkeene@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{EventMachine Kafka driver}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency "eventmachine-le", ">= 1.1.5"
|
19
|
+
s.add_dependency "yajl-ruby", ">= 0.8.2"
|
20
|
+
s.add_development_dependency "rspec", "~> 2.6.0"
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class Client
|
4
|
+
def initialize(host, port)
|
5
|
+
@host = host || 'localhost'
|
6
|
+
@port = port || 9092
|
7
|
+
@callback = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_data(data)
|
11
|
+
connect if @connection.nil? || @connection.disconnected?
|
12
|
+
@connection.send_data(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_data(&block)
|
16
|
+
@callback = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def connect
|
20
|
+
@connection = EM.connect(@host, @port, EM::Kafka::Connection)
|
21
|
+
@connection.on(:message) do |message|
|
22
|
+
@callback.call(message) if @callback
|
23
|
+
end
|
24
|
+
@connection
|
25
|
+
end
|
26
|
+
|
27
|
+
def close_connection
|
28
|
+
@connection.close_connection_after_writing
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EventMachine::Kafka
|
2
|
+
class Connection < EM::Connection
|
3
|
+
include EventMachine::Kafka::EventEmitter
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@disconnected = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def disconnected?
|
11
|
+
@disconnected
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection_completed
|
15
|
+
EventMachine::Kafka.logger.info("Connected to Kafka")
|
16
|
+
emit(:connected)
|
17
|
+
end
|
18
|
+
|
19
|
+
def receive_data(data)
|
20
|
+
emit(:message, data)
|
21
|
+
end
|
22
|
+
|
23
|
+
def unbind
|
24
|
+
@disconnected = true
|
25
|
+
EventMachine::Kafka.logger.info("Disconnected from Kafka")
|
26
|
+
emit(:closed)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class Consumer
|
4
|
+
require_relative "consumer_request"
|
5
|
+
require_relative "parser"
|
6
|
+
|
7
|
+
attr_accessor :topic,
|
8
|
+
:partition,
|
9
|
+
:offset,
|
10
|
+
:max_size,
|
11
|
+
:request_type,
|
12
|
+
:polling,
|
13
|
+
:client,
|
14
|
+
:host,
|
15
|
+
:port
|
16
|
+
|
17
|
+
def initialize(uri, options = {})
|
18
|
+
uri = URI(uri)
|
19
|
+
self.host = uri.host
|
20
|
+
self.port = uri.port
|
21
|
+
self.topic = uri.user
|
22
|
+
self.partition = uri.path[1..-1].to_i
|
23
|
+
|
24
|
+
self.offset = options[:offset] || 0
|
25
|
+
self.max_size = options[:max_size] || EM::Kafka::MESSAGE_MAX_SIZE
|
26
|
+
self.request_type = options[:request_type] || EM::Kafka::REQUEST_FETCH
|
27
|
+
self.polling = options[:polling] || EM::Kafka::CONSUMER_POLLING_INTERVAL
|
28
|
+
self.client = EM::Kafka::Client.new(host, port)
|
29
|
+
client.connect
|
30
|
+
end
|
31
|
+
|
32
|
+
def consume(&block)
|
33
|
+
raise ArgumentError.new("block required") unless block_given?
|
34
|
+
parser = EM::Kafka::Parser.new(offset, &block)
|
35
|
+
parser.on_offset_update { |i| self.offset = i }
|
36
|
+
client.on_data { |binary| parser.on_data(binary) }
|
37
|
+
EM.add_periodic_timer(polling) { request_consume }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def request_consume
|
43
|
+
request = EM::Kafka::ConsumerRequest.new(request_type, topic, partition, offset, max_size)
|
44
|
+
client.send_data(request.encode_size)
|
45
|
+
client.send_data(request.encode)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class ConsumerRequest
|
4
|
+
def initialize(type, topic, partition, offset, max_size)
|
5
|
+
@type, @topic, @partition, @offset, @max_size =
|
6
|
+
type, topic, partition, offset, max_size
|
7
|
+
end
|
8
|
+
|
9
|
+
def encode_size
|
10
|
+
[2 + 2 + @topic.length + 4 + 8 + 4].pack("N")
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode
|
14
|
+
[@type].pack("n") +
|
15
|
+
[@topic.length].pack("n") +
|
16
|
+
@topic +
|
17
|
+
[@partition].pack("N") +
|
18
|
+
[@offset].pack("Q").reverse + # DIY 64bit big endian integer
|
19
|
+
[@max_size].pack("N")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EventMachine::Kafka
|
2
|
+
module EventEmitter
|
3
|
+
def on(event, &listener)
|
4
|
+
_listeners[event] << listener
|
5
|
+
end
|
6
|
+
|
7
|
+
def emit(event, *args)
|
8
|
+
_listeners[event].each { |l| l.call(*args) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def remove_listener(event, &listener)
|
12
|
+
_listeners[event].delete(listener)
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove_all_listeners(event)
|
16
|
+
_listeners.delete(event)
|
17
|
+
end
|
18
|
+
|
19
|
+
def listeners(event)
|
20
|
+
_listeners[event]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def _listeners
|
26
|
+
@_listeners ||= Hash.new { |h,k| h[k] = [] }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
# 1 byte "magic" identifier to allow format changes
|
4
|
+
# 4 byte CRC32 of the payload
|
5
|
+
# N - 5 byte payload
|
6
|
+
class Message
|
7
|
+
require "zlib"
|
8
|
+
attr_accessor :magic, :checksum, :payload, :size
|
9
|
+
|
10
|
+
def initialize(payload, magic = 0, checksum = nil, size = nil)
|
11
|
+
self.payload = payload
|
12
|
+
self.magic = magic
|
13
|
+
self.checksum = checksum || Zlib.crc32(payload)
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
checksum == Zlib.crc32(payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
def encode
|
21
|
+
[magic, checksum].pack("CN") +
|
22
|
+
payload.to_s.force_encoding(Encoding::ASCII_8BIT)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.decode(size, binary)
|
26
|
+
return unless binary
|
27
|
+
magic = binary[4].unpack("C").shift
|
28
|
+
checksum = binary[5..9].unpack("N").shift
|
29
|
+
payload = binary[9..-1]
|
30
|
+
new(payload, magic, checksum)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class Parser
|
4
|
+
attr_accessor :offset
|
5
|
+
|
6
|
+
def initialize(offset = 0, &block)
|
7
|
+
self.offset = offset
|
8
|
+
@block = block
|
9
|
+
reset
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_data(binary)
|
13
|
+
if @complete
|
14
|
+
parsed_size = binary[0, 4].unpack("N").shift
|
15
|
+
|
16
|
+
if (parsed_size - 2) > 0
|
17
|
+
@size = parsed_size
|
18
|
+
else
|
19
|
+
# empty response
|
20
|
+
return
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
@buffer << binary
|
25
|
+
|
26
|
+
received_data = @buffer.size + binary.size
|
27
|
+
if received_data >= @size
|
28
|
+
parse(@buffer[6, @size]) # account for 4 byte size and 2 byte junk
|
29
|
+
else
|
30
|
+
@complete = false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_offset_update(&callback)
|
35
|
+
@on_offset_update_callback = callback
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def parse(frame)
|
41
|
+
i = 0
|
42
|
+
while i <= frame.length do
|
43
|
+
break unless message_size = frame[i, 4].unpack("N").first
|
44
|
+
message_data = frame[i, message_size + 4]
|
45
|
+
message = Kafka::Message.decode(message_size, message_data)
|
46
|
+
i += message_size + 4
|
47
|
+
@block.call(message)
|
48
|
+
end
|
49
|
+
|
50
|
+
advance_offset(i)
|
51
|
+
reset
|
52
|
+
end
|
53
|
+
|
54
|
+
def reset
|
55
|
+
@size = 0
|
56
|
+
@complete = true
|
57
|
+
@buffer = ""
|
58
|
+
end
|
59
|
+
|
60
|
+
def advance_offset(i)
|
61
|
+
self.offset += i
|
62
|
+
@on_offset_update_callback.call(offset) if @on_offset_update_callback
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class Producer
|
4
|
+
require_relative "producer_request"
|
5
|
+
attr_accessor :host, :port, :topic, :partition, :client
|
6
|
+
|
7
|
+
def initialize(uri)
|
8
|
+
uri = URI(uri)
|
9
|
+
self.host = uri.host
|
10
|
+
self.port = uri.port
|
11
|
+
self.topic = uri.user
|
12
|
+
self.partition = uri.path[1..-1].to_i
|
13
|
+
|
14
|
+
raise ArgumentError("topic required") unless topic
|
15
|
+
|
16
|
+
self.client = EM::Kafka::Client.new(host, port)
|
17
|
+
client.connect
|
18
|
+
end
|
19
|
+
|
20
|
+
def deliver(message)
|
21
|
+
request = EM::Kafka::ProducerRequest.new(topic, partition, message)
|
22
|
+
client.send_data(request.encode)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Kafka
|
3
|
+
class ProducerRequest
|
4
|
+
def initialize(topic, partition, messages)
|
5
|
+
@topic, @partition, @messages = topic, partition, messages
|
6
|
+
end
|
7
|
+
|
8
|
+
def encode
|
9
|
+
data = "\x00\x00" +
|
10
|
+
[@topic.length].pack("n") +
|
11
|
+
@topic +
|
12
|
+
[@partition].pack("N") +
|
13
|
+
encode_messages(@messages)
|
14
|
+
|
15
|
+
[data.length].pack("N") + data
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def encode_messages(messages)
|
21
|
+
messages = [messages].flatten
|
22
|
+
messages = messages.map do |m|
|
23
|
+
if m.is_a?(EM::Kafka::Message)
|
24
|
+
m
|
25
|
+
else
|
26
|
+
EM::Kafka::Message.new(Yajl::Encoder.encode(m))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
message_set = messages.map { |m|
|
31
|
+
data = m.encode
|
32
|
+
[data.length].pack("N") + data
|
33
|
+
}.join("")
|
34
|
+
[message_set.length].pack("N") + message_set
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/em-kafka.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "logger"
|
3
|
+
require "uri"
|
4
|
+
require "yajl"
|
5
|
+
|
6
|
+
require_relative "em-kafka/event_emitter"
|
7
|
+
require_relative "em-kafka/connection"
|
8
|
+
require_relative "em-kafka/client"
|
9
|
+
require_relative "em-kafka/message"
|
10
|
+
require_relative "em-kafka/producer"
|
11
|
+
require_relative "em-kafka/consumer"
|
12
|
+
|
13
|
+
module EventMachine
|
14
|
+
module Kafka
|
15
|
+
MESSAGE_MAX_SIZE = 1048576 # 1 MB
|
16
|
+
CONSUMER_POLLING_INTERVAL = 2 # 2 seconds
|
17
|
+
|
18
|
+
REQUEST_PRODUCE = 0
|
19
|
+
REQUEST_FETCH = 1
|
20
|
+
REQUEST_MULTIFETCH = 2
|
21
|
+
REQUEST_MULTIPRODUCE = 3
|
22
|
+
REQUEST_OFFSETS = 4
|
23
|
+
|
24
|
+
ERROR_NO_ERROR = 0
|
25
|
+
ERROR_OFFSET_OUT_OF_RANGE = 1
|
26
|
+
ERROR_INVALID_MESSAGE_CODE = 2
|
27
|
+
ERROR_WRONG_PARTITION_CODE = 3
|
28
|
+
ERROR_INVALID_RETCH_SIZE_CODE = 4
|
29
|
+
|
30
|
+
ERROR_DESCRIPTIONS = {
|
31
|
+
ERROR_NO_ERROR => 'No error',
|
32
|
+
ERROR_INVALID_MESSAGE_CODE => 'Offset out of range',
|
33
|
+
ERROR_INVALID_MESSAGE_CODE => 'Invalid message code',
|
34
|
+
ERROR_WRONG_PARTITION_CODE => 'Wrong partition code',
|
35
|
+
ERROR_INVALID_RETCH_SIZE_CODE => 'Invalid retch size code'
|
36
|
+
}
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def logger
|
40
|
+
@logger ||= Logger.new(STDOUT)
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger=(new_logger)
|
44
|
+
@logger = new_logger
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EM::Kafka::ConsumerRequest do
|
4
|
+
before do
|
5
|
+
@request = EM::Kafka::ConsumerRequest.new(
|
6
|
+
EM::Kafka::REQUEST_FETCH,
|
7
|
+
"topic",
|
8
|
+
0,
|
9
|
+
100,
|
10
|
+
EM::Kafka::MESSAGE_MAX_SIZE
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#encode" do
|
15
|
+
it "returns binary" do
|
16
|
+
data = [EM::Kafka::REQUEST_FETCH].pack("n") +
|
17
|
+
["topic".length].pack('n') +
|
18
|
+
"topic" +
|
19
|
+
[0].pack("N") +
|
20
|
+
[100].pack("Q").reverse + # DIY 64bit big endian integer
|
21
|
+
[EM::Kafka::MESSAGE_MAX_SIZE].pack("N")
|
22
|
+
|
23
|
+
@request.encode.should == data
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#encode_size" do
|
28
|
+
it "returns packed 2 + 2 + @topic.length + 4 + 8 + 4" do
|
29
|
+
@request.encode_size.should == "\x00\x00\x00\x19"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EM::Kafka::Consumer do
|
4
|
+
before do
|
5
|
+
@client = mock("Client", :connect => true)
|
6
|
+
EM::Kafka::Client.should_receive(:new).and_return(@client)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should set a topic and partition on initialize" do
|
10
|
+
consumer = EM::Kafka::Consumer.new("kafka://testing@localhost:9092/3")
|
11
|
+
consumer.host.should == "localhost"
|
12
|
+
consumer.port.should == 9092
|
13
|
+
consumer.topic.should == "testing"
|
14
|
+
consumer.partition.should == 3
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should set default partition to 0" do
|
18
|
+
consumer = EM::Kafka::Consumer.new("kafka://testing@localhost:9092")
|
19
|
+
consumer.host.should == "localhost"
|
20
|
+
consumer.port.should == 9092
|
21
|
+
consumer.topic.should == "testing"
|
22
|
+
consumer.partition.should == 0
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EM::Kafka::Message do
|
4
|
+
describe "#encode" do
|
5
|
+
it "turns Message into data" do
|
6
|
+
message = EM::Kafka::Message.new("ale")
|
7
|
+
message.payload.should == "ale"
|
8
|
+
message.checksum.should == 1120192889
|
9
|
+
message.magic.should == 0
|
10
|
+
|
11
|
+
message.encode.should == [0].pack("C") +
|
12
|
+
[1120192889].pack("N") +
|
13
|
+
"ale".force_encoding(Encoding::ASCII_8BIT)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".decode" do
|
18
|
+
it "turns data into a Message" do
|
19
|
+
data = [12].pack("N") +
|
20
|
+
[0].pack("C") +
|
21
|
+
[1120192889].pack("N") + "ale"
|
22
|
+
|
23
|
+
message = EM::Kafka::Message.decode(data.size, data)
|
24
|
+
message.should be_valid
|
25
|
+
message.payload.should == "ale"
|
26
|
+
message.checksum.should == 1120192889
|
27
|
+
message.magic.should == 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EM::Kafka::Parser do
|
4
|
+
describe "parse" do
|
5
|
+
it "parses messages from newline boundaries across packets" do
|
6
|
+
messages = []
|
7
|
+
parser = EM::Kafka::Parser.new do |message|
|
8
|
+
messages << message
|
9
|
+
end
|
10
|
+
|
11
|
+
message_1 = EM::Kafka::Message.new("foo").encode
|
12
|
+
message_2 = EM::Kafka::Message.new("barizzle").encode
|
13
|
+
message_3 = EM::Kafka::Message.new("langlang").encode
|
14
|
+
|
15
|
+
binary = [51].pack("N") +
|
16
|
+
[0, 0].pack("CC") + # 2 byte offset
|
17
|
+
[message_1.size].pack("N") +
|
18
|
+
message_1 +
|
19
|
+
[message_2.size].pack("N") +
|
20
|
+
message_2 +
|
21
|
+
[message_3.size].pack("N") +
|
22
|
+
message_3
|
23
|
+
|
24
|
+
frame_1 = binary[0..11]
|
25
|
+
frame_2 = binary[12..-1]
|
26
|
+
|
27
|
+
parser.on_data(frame_1)
|
28
|
+
parser.on_data(frame_2)
|
29
|
+
|
30
|
+
messages[0].payload.should == "foo"
|
31
|
+
messages[0].should be_valid
|
32
|
+
messages[1].payload.should == "barizzle"
|
33
|
+
messages[1].should be_valid
|
34
|
+
messages[2].payload.should == "langlang"
|
35
|
+
messages[2].should be_valid
|
36
|
+
|
37
|
+
empty_frame = [2, 0, 0].pack("NCC")
|
38
|
+
parser.on_data(empty_frame)
|
39
|
+
|
40
|
+
message_4 = EM::Kafka::Message.new("after empty").encode
|
41
|
+
|
42
|
+
binary = [26].pack("N") +
|
43
|
+
[0, 0].pack("CC") + # 2 byte offset
|
44
|
+
[message_4.size].pack("N") +
|
45
|
+
message_4
|
46
|
+
|
47
|
+
frame_3 = binary
|
48
|
+
parser.on_data(frame_3)
|
49
|
+
|
50
|
+
messages[3].payload.should == "after empty"
|
51
|
+
messages[3].should be_valid
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "on_offset_update" do
|
56
|
+
it "returns the proper offset" do
|
57
|
+
offset = 0
|
58
|
+
messages = []
|
59
|
+
parser = EM::Kafka::Parser.new do |message|
|
60
|
+
messages << message
|
61
|
+
end
|
62
|
+
parser.on_offset_update {|new_offset| offset = new_offset }
|
63
|
+
|
64
|
+
message_1 = EM::Kafka::Message.new("foo").encode
|
65
|
+
message_2 = EM::Kafka::Message.new("barizzle").encode
|
66
|
+
message_3 = EM::Kafka::Message.new("langlang").encode
|
67
|
+
|
68
|
+
binary = [51].pack("N") +
|
69
|
+
[0, 0].pack("CC") + # 2 byte offset
|
70
|
+
[message_1.size].pack("N") +
|
71
|
+
message_1 +
|
72
|
+
[message_2.size].pack("N") +
|
73
|
+
message_2 +
|
74
|
+
[message_3.size].pack("N") +
|
75
|
+
message_3
|
76
|
+
|
77
|
+
frame_1 = binary[0..11]
|
78
|
+
frame_2 = binary[12..-1]
|
79
|
+
|
80
|
+
parser.on_data(frame_1)
|
81
|
+
parser.on_data(frame_2)
|
82
|
+
|
83
|
+
offset.should == message_1.size + message_2.size + message_3.size + 4 + 4 + 4
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EM::Kafka::ProducerRequest do
|
4
|
+
describe "#encode" do
|
5
|
+
it "binary encodes an empty request" do
|
6
|
+
bytes = EM::Kafka::ProducerRequest.new("test", 0, []).encode
|
7
|
+
bytes.length.should eql(20)
|
8
|
+
bytes.should eql("\000\000\000\020\000\000\000\004test\000\000\000\000\000\000\000\000")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should binary encode a request with a message, using a specific wire format" do
|
12
|
+
request = EM::Kafka::ProducerRequest.new("test", 3, EM::Kafka::Message.new("ale"))
|
13
|
+
bytes = request.encode
|
14
|
+
|
15
|
+
data_size = bytes[0, 4].unpack("N").shift
|
16
|
+
request_id = bytes[4, 2].unpack("n").shift
|
17
|
+
topic_length = bytes[6, 2].unpack("n").shift
|
18
|
+
topic = bytes[8, 4]
|
19
|
+
partition = bytes[12, 4].unpack("N").shift
|
20
|
+
messages_length = bytes[16, 4].unpack("N").shift
|
21
|
+
messages = bytes[20, messages_length]
|
22
|
+
|
23
|
+
bytes.length.should eql(32)
|
24
|
+
data_size.should eql(28)
|
25
|
+
request_id.should eql(0)
|
26
|
+
topic_length.should eql(4)
|
27
|
+
topic.should eql("test")
|
28
|
+
partition.should eql(3)
|
29
|
+
messages_length.should eql(12)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "encodes ruby objects to JSON and inflates message" do
|
33
|
+
message = EM::Kafka::Message.new(Yajl::Encoder.encode(key: "value"))
|
34
|
+
request_with_message = EM::Kafka::ProducerRequest.new("test", 3, message)
|
35
|
+
request_with_message.encode.size.should == 44
|
36
|
+
|
37
|
+
request = EM::Kafka::ProducerRequest.new("test", 3, key: "value")
|
38
|
+
request.encode.size.should == 44
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe EM::Kafka::Producer do
|
4
|
+
before do
|
5
|
+
@client = mock("Client", :connect => true)
|
6
|
+
EM::Kafka::Client.should_receive(:new).and_return(@client)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should set a topic and partition on initialize" do
|
10
|
+
producer = EM::Kafka::Producer.new("kafka://testing@localhost:9092/3")
|
11
|
+
producer.host.should == "localhost"
|
12
|
+
producer.port.should == 9092
|
13
|
+
producer.topic.should == "testing"
|
14
|
+
producer.partition.should == 3
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should set default partition to 0" do
|
18
|
+
producer = EM::Kafka::Producer.new("kafka://testing@localhost:9092")
|
19
|
+
producer.host.should == "localhost"
|
20
|
+
producer.port.should == 9092
|
21
|
+
producer.topic.should == "testing"
|
22
|
+
producer.partition.should == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should send messages" do
|
26
|
+
producer = EM::Kafka::Producer.new("kafka://testing@localhost:9092/3")
|
27
|
+
message = EM::Kafka::Message.new("hello world")
|
28
|
+
request = EM::Kafka::ProducerRequest.new("testing", 3, message)
|
29
|
+
|
30
|
+
@client.should_receive(:send_data).with(request.encode)
|
31
|
+
|
32
|
+
producer.deliver(message)
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-kafka
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brandon Keene
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: eventmachine-le
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.5
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.5
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yajl-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.8.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.8.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.6.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.6.0
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- bkeene@gmail.com
|
58
|
+
executables:
|
59
|
+
- consume
|
60
|
+
- produce
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- .gitignore
|
65
|
+
- .rspec
|
66
|
+
- Gemfile
|
67
|
+
- Gemfile.lock
|
68
|
+
- LICENSE
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- bin/consume
|
72
|
+
- bin/produce
|
73
|
+
- em-kafka.gemspec
|
74
|
+
- lib/em-kafka.rb
|
75
|
+
- lib/em-kafka/client.rb
|
76
|
+
- lib/em-kafka/connection.rb
|
77
|
+
- lib/em-kafka/consumer.rb
|
78
|
+
- lib/em-kafka/consumer_request.rb
|
79
|
+
- lib/em-kafka/event_emitter.rb
|
80
|
+
- lib/em-kafka/message.rb
|
81
|
+
- lib/em-kafka/parser.rb
|
82
|
+
- lib/em-kafka/producer.rb
|
83
|
+
- lib/em-kafka/producer_request.rb
|
84
|
+
- lib/em-kafka/version.rb
|
85
|
+
- spec/em-kafka/consumer_request_spec.rb
|
86
|
+
- spec/em-kafka/consumer_spec.rb
|
87
|
+
- spec/em-kafka/message_spec.rb
|
88
|
+
- spec/em-kafka/parser_spec.rb
|
89
|
+
- spec/em-kafka/producer_request_spec.rb
|
90
|
+
- spec/em-kafka/producer_spec.rb
|
91
|
+
- spec/em-kafka_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
homepage: ''
|
94
|
+
licenses: []
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.0.3
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: EventMachine Kafka driver
|
116
|
+
test_files: []
|
117
|
+
has_rdoc:
|