ruby-kafka 0.1.0.pre.alpha → 0.1.0.pre.alpha2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a697874e8d60e20fefd56d530fae7af5d9a0122
4
- data.tar.gz: c93b6564dde9e1630a1675a13dbc4227f4aa0053
3
+ metadata.gz: 945e87a87fcfebd2808de4203027613846a2f7ad
4
+ data.tar.gz: f43c576a56aea2f49ef1047aec56f376ec2be0b0
5
5
  SHA512:
6
- metadata.gz: e64575cd8677e03e565f9ed64d7b9d6b548a80ab68c7f37d108a0f6d95161843acef2805a625db9d9502d5861fe166200b048119835209162235d9c4783d679b
7
- data.tar.gz: 4caa3012931623f4f3372368c195736aee833995b76937b7e2dd8bea5dec2fa9ce31dc1bbb209d435144884a158e674b6fd55fb5087a0f971cc9245c678229cd
6
+ metadata.gz: c51ccf72b3822a773d013c68fe77665d7d5e9b4021131cb97d8bed65e47c681be18d89f718b63840ef5c463991b0c56c042b3ab747753c23b7f88c0b6131d8d3
7
+ data.tar.gz: 4c5c41e3858562fcdac0540652f5d3d8f21334a841b717f7f6281f1ff15a58c3ba165f518fbce9778669d5d35279e5242bb3ca3d1afce62a5c40c01b790e1720
data/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # Kafka
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/kafka`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A Ruby client library for the Kafka distributed log system. The focus of this library will be operational simplicity, with good logging and metrics that can make debugging issues easier.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ This library is still in pre-alpha stage, but development is ongoing. Current efforts are focused on implementing a solid Producer client. The next step will be implementing a client for the Kafka 0.9 Consumer API.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
- gem 'kafka'
12
+ gem 'ruby-kafka'
13
13
  ```
14
14
 
15
15
  And then execute:
@@ -18,11 +18,26 @@ And then execute:
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install kafka
21
+ $ gem install ruby-kafka
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ ```ruby
26
+ kafka = Kafka.new(
27
+ seed_brokers: ["kafka1:9092", "kafka2:9092"],
28
+ client_id: "my-app",
29
+ logger: Logger.new($stderr),
30
+ )
31
+
32
+ producer = kafka.get_producer
33
+
34
+ # `write` will buffer the message in the producer.
35
+ producer.write("hello1", key: "x", topic: "test-messages", partition: 0)
36
+ producer.write("hello2", key: "y", topic: "test-messages", partition: 1)
37
+
38
+ # `flush` will send the buffered messages to the cluster.
39
+ producer.flush
40
+ ```
26
41
 
27
42
  ## Development
28
43
 
@@ -32,7 +47,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
47
 
33
48
  ## Contributing
34
49
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/kafka.
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zendesk/ruby-kafka.
36
51
 
37
52
 
38
53
  ## Copyright and license
@@ -0,0 +1,35 @@
1
+ zookeeper:
2
+ image: wurstmeister/zookeeper
3
+ ports:
4
+ - "2181"
5
+ kafka1:
6
+ image: wurstmeister/kafka:0.8.2.0
7
+ ports:
8
+ - "9992:9092"
9
+ links:
10
+ - zookeeper:zk
11
+ environment:
12
+ KAFKA_ADVERTISED_HOST_NAME: "192.168.42.45"
13
+ KAFKA_CREATE_TOPICS: "test-messages:5:3"
14
+ volumes:
15
+ - /var/run/docker.sock:/var/run/docker.sock
16
+ kafka2:
17
+ image: wurstmeister/kafka:0.8.2.0
18
+ ports:
19
+ - "9993:9092"
20
+ links:
21
+ - zookeeper:zk
22
+ environment:
23
+ KAFKA_ADVERTISED_HOST_NAME: "192.168.42.45"
24
+ volumes:
25
+ - /var/run/docker.sock:/var/run/docker.sock
26
+ kafka3:
27
+ image: wurstmeister/kafka:0.8.2.0
28
+ ports:
29
+ - "9994:9092"
30
+ links:
31
+ - zookeeper:zk
32
+ environment:
33
+ KAFKA_ADVERTISED_HOST_NAME: "192.168.42.45"
34
+ volumes:
35
+ - /var/run/docker.sock:/var/run/docker.sock
@@ -1,9 +1,19 @@
1
1
  require "kafka/version"
2
- require "kafka/cluster"
3
- require "kafka/producer"
2
+ require "kafka/client"
4
3
 
5
4
  module Kafka
5
+ Error = Class.new(StandardError)
6
+ ConnectionError = Class.new(Error)
7
+ CorruptMessage = Class.new(Error)
8
+ UnknownError = Class.new(Error)
9
+ OffsetOutOfRange = Class.new(Error)
10
+ UnknownTopicOrPartition = Class.new(Error)
11
+ InvalidMessageSize = Class.new(Error)
12
+ LeaderNotAvailable = Class.new(Error)
13
+ NotLeaderForPartition = Class.new(Error)
14
+ RequestTimedOut = Class.new(Error)
15
+
6
16
  def self.new(**options)
7
- Cluster.new(**options)
17
+ Client.new(**options)
8
18
  end
9
19
  end
@@ -0,0 +1,50 @@
1
+ require "logger"
2
+ require "kafka/connection"
3
+ require "kafka/protocol"
4
+
5
+ module Kafka
6
+ class Broker
7
+ def initialize(host:, port:, node_id: nil, client_id:, logger:)
8
+ @host, @port, @node_id = host, port, node_id
9
+
10
+ @connection = Connection.new(
11
+ host: host,
12
+ port: port,
13
+ client_id: client_id,
14
+ logger: logger
15
+ )
16
+
17
+ @logger = logger
18
+ end
19
+
20
+ def to_s
21
+ "#{@host}:#{@port} (node_id=#{@node_id.inspect})"
22
+ end
23
+
24
+ def fetch_metadata(**options)
25
+ api_key = Protocol::TOPIC_METADATA_API_KEY
26
+ request = Protocol::TopicMetadataRequest.new(**options)
27
+ response_class = Protocol::MetadataResponse
28
+
29
+ response = @connection.request(api_key, request, response_class)
30
+
31
+ response.topics.each do |topic|
32
+ Protocol.handle_error(topic.topic_error_code)
33
+
34
+ topic.partitions.each do |partition|
35
+ Protocol.handle_error(partition.partition_error_code)
36
+ end
37
+ end
38
+
39
+ response
40
+ end
41
+
42
+ def produce(**options)
43
+ api_key = Protocol::PRODUCE_API_KEY
44
+ request = Protocol::ProduceRequest.new(**options)
45
+ response_class = request.requires_acks? ? Protocol::ProduceResponse : nil
46
+
47
+ @connection.request(api_key, request, response_class)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ require "kafka/broker"
2
+
3
+ module Kafka
4
+
5
+ # A broker pool represents the set of brokers in a cluster. It needs to be initialized
6
+ # with a non-empty list of seed brokers. The first seed broker that the pool can connect
7
+ # to will be asked for the cluster metadata, which allows the pool to map topic
8
+ # partitions to the current leader for those partitions.
9
+ class BrokerPool
10
+
11
+ # Initializes a broker pool with a set of seed brokers.
12
+ #
13
+ # The pool will try to fetch cluster metadata from one of the brokers.
14
+ #
15
+ # @param seed_brokers [Array<String>]
16
+ # @param client_id [String]
17
+ # @param logger [Logger]
18
+ def initialize(seed_brokers:, client_id:, logger:)
19
+ @client_id = client_id
20
+ @logger = logger
21
+ @brokers = {}
22
+
23
+ initialize_from_seed_brokers(seed_brokers)
24
+ end
25
+
26
+ # Gets the leader of the given topic and partition.
27
+ #
28
+ # @param topic [String]
29
+ # @param partition [Integer]
30
+ # @return [Broker] the broker that's currently acting as leader of the partition.
31
+ def get_leader(topic, partition)
32
+ leader_id = @cluster_info.find_leader_id(topic, partition)
33
+
34
+ broker_for_id(leader_id)
35
+ end
36
+
37
+ private
38
+
39
+ def broker_for_id(broker_id)
40
+ @brokers[broker_id] ||= connect_to_broker(broker_id)
41
+ end
42
+
43
+ def connect_to_broker(broker_id)
44
+ broker_info = @cluster_info.find_broker(broker_id)
45
+
46
+ Broker.new(
47
+ host: broker_info.host,
48
+ port: broker_info.port,
49
+ node_id: broker_info.node_id,
50
+ client_id: @client_id,
51
+ logger: @logger,
52
+ )
53
+ end
54
+
55
+ def initialize_from_seed_brokers(seed_brokers)
56
+ seed_brokers.each do |node|
57
+ @logger.info "Trying to initialize broker pool from node #{node}"
58
+
59
+ begin
60
+ host, port = node.split(":", 2)
61
+
62
+ broker = Broker.new(host: host, port: port, client_id: @client_id, logger: @logger)
63
+
64
+ @cluster_info = broker.fetch_metadata
65
+
66
+ @logger.info "Initialized broker pool with brokers: #{@cluster_info.brokers.inspect}"
67
+
68
+ return
69
+ rescue Error => e
70
+ @logger.error "Failed to fetch metadata from broker #{broker}: #{e}"
71
+ end
72
+ end
73
+
74
+ raise ConnectionError, "Could not connect to any of the seed brokers: #{seed_brokers.inspect}"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,22 @@
1
+ require "kafka/broker_pool"
2
+ require "kafka/producer"
3
+
4
+ module Kafka
5
+ class Client
6
+ def initialize(seed_brokers:, client_id:, logger:)
7
+ @seed_brokers = seed_brokers
8
+ @client_id = client_id
9
+ @logger = logger
10
+ end
11
+
12
+ def get_producer(**options)
13
+ broker_pool = BrokerPool.new(
14
+ seed_brokers: @seed_brokers,
15
+ client_id: @client_id,
16
+ logger: @logger
17
+ )
18
+
19
+ Producer.new(broker_pool: broker_pool, logger: @logger, **options)
20
+ end
21
+ end
22
+ end
@@ -4,64 +4,109 @@ require "kafka/protocol/encoder"
4
4
  require "kafka/protocol/decoder"
5
5
 
6
6
  module Kafka
7
- ConnectionError = Class.new(StandardError)
8
7
 
8
+ # A connection to a single Kafka broker.
9
+ #
10
+ # Usually you'll need a separate connection to each broker in a cluster, since most
11
+ # requests must be directed specifically to the broker that is currently leader for
12
+ # the set of topic partitions you want to produce to or consumer from.
9
13
  class Connection
14
+ API_VERSION = 0
15
+
16
+ # Opens a connection to a Kafka broker.
17
+ #
18
+ # @param host [String] the hostname of the broker.
19
+ # @param port [Integer] the port of the broker.
20
+ # @param client_id [String] the client id is a user-specified string sent in each
21
+ # request to help trace calls and should logically identify the application
22
+ # making the request.
23
+ # @param logger [Logger] the logger used to log trace messages.
24
+ #
25
+ # @return [Connection] a new connection.
10
26
  def initialize(host:, port:, client_id:, logger:)
11
- @host = host
12
- @port = port
13
- @client_id = client_id
27
+ @host, @port, @client_id = host, port, client_id
14
28
  @logger = logger
15
- @socket = nil
16
- @correlation_id = 0
17
- @socket_timeout = 1000
18
- end
19
29
 
20
- def open
21
30
  @logger.info "Opening connection to #{@host}:#{@port} with client id #{@client_id}..."
22
31
 
23
- @socket = TCPSocket.new(@host, @port)
32
+ @socket = TCPSocket.new(host, port)
33
+
34
+ @encoder = Kafka::Protocol::Encoder.new(@socket)
35
+ @decoder = Kafka::Protocol::Decoder.new(@socket)
36
+
37
+ # Correlation id is initialized to zero and bumped for each request.
38
+ @correlation_id = 0
24
39
  rescue SocketError => e
25
- @logger.error "Failed to connect to #{@host}:#{@port}: #{e}"
40
+ @logger.error "Failed to connect to #{host}:#{port}: #{e}"
26
41
 
27
42
  raise ConnectionError, e
28
43
  end
29
44
 
45
+ def to_s
46
+ "#{@host}:#{@port}"
47
+ end
48
+
49
+ # Sends a request over the connection.
50
+ #
51
+ # @param api_key [Integer] the integer code for the API that is invoked.
52
+ # @param request [#encode] the request that should be encoded and written.
53
+ # @param response_class [#decode] an object that can decode the response.
54
+ #
55
+ # @return [Object] the response that was decoded by `response_class`.
56
+ def request(api_key, request, response_class)
57
+ write_request(api_key, request)
58
+
59
+ unless response_class.nil?
60
+ read_response(response_class)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Writes a request over the connection.
67
+ #
68
+ # @param api_key [Integer] the integer code for the API that is invoked.
69
+ # @param request [#encode] the request that should be encoded and written.
70
+ #
71
+ # @return [nil]
30
72
  def write_request(api_key, request)
31
73
  @correlation_id += 1
74
+ @logger.debug "Sending request #{@correlation_id} to #{to_s}"
32
75
 
33
76
  message = Kafka::Protocol::RequestMessage.new(
34
77
  api_key: api_key,
35
- api_version: 0,
78
+ api_version: API_VERSION,
36
79
  correlation_id: @correlation_id,
37
80
  client_id: @client_id,
38
81
  request: request,
39
82
  )
40
83
 
41
- buffer = StringIO.new
42
- message_encoder = Kafka::Protocol::Encoder.new(buffer)
43
- message.encode(message_encoder)
44
-
45
- @logger.info "Sending request #{@correlation_id} (#{request.class})..."
84
+ data = Kafka::Protocol::Encoder.encode_with(message)
85
+ @encoder.write_bytes(data)
46
86
 
47
- connection_encoder = Kafka::Protocol::Encoder.new(@socket)
48
- connection_encoder.write_bytes(buffer.string)
87
+ nil
49
88
  end
50
89
 
51
- def read_response(response)
52
- @logger.info "Reading response #{response.class}"
90
+ # Reads a response from the connection.
91
+ #
92
+ # @param response_class [#decode] an object that can decode the response from
93
+ # a given Decoder.
94
+ #
95
+ # @return [nil]
96
+ def read_response(response_class)
97
+ @logger.debug "Waiting for response #{@correlation_id} from #{to_s}"
53
98
 
54
- connection_decoder = Kafka::Protocol::Decoder.new(@socket)
55
- bytes = connection_decoder.bytes
99
+ bytes = @decoder.bytes
56
100
 
57
101
  buffer = StringIO.new(bytes)
58
102
  response_decoder = Kafka::Protocol::Decoder.new(buffer)
59
103
 
60
104
  correlation_id = response_decoder.int32
105
+ response = response_class.decode(response_decoder)
61
106
 
62
- @logger.info "Correlation id #{correlation_id}"
107
+ @logger.debug "Received response #{correlation_id} from #{to_s}"
63
108
 
64
- response.decode(response_decoder)
109
+ response
65
110
  end
66
111
  end
67
112
  end
@@ -0,0 +1,12 @@
1
+ module Kafka
2
+ class Message
3
+ attr_reader :value, :key, :topic, :partition
4
+
5
+ def initialize(value, key:, topic:, partition:)
6
+ @value = value
7
+ @key = key
8
+ @topic = topic
9
+ @partition = partition
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ require "kafka/protocol/message"
2
+
3
+ module Kafka
4
+ class MessageSet
5
+ def initialize(messages)
6
+ @messages = messages
7
+ end
8
+
9
+ def to_h
10
+ hsh = {}
11
+
12
+ @messages.each do |message|
13
+ value, key = message.value, message.key
14
+ topic, partition = message.topic, message.partition
15
+
16
+ hsh[topic] ||= {}
17
+ hsh[topic][partition] ||= []
18
+ hsh[topic][partition] << Protocol::Message.new(value: value, key: key)
19
+ end
20
+
21
+ hsh
22
+ end
23
+ end
24
+ end
@@ -1,4 +1,5 @@
1
- require "kafka/protocol/message"
1
+ require "kafka/message"
2
+ require "kafka/message_set"
2
3
 
3
4
  module Kafka
4
5
  class Producer
@@ -6,30 +7,49 @@ module Kafka
6
7
  # acknowledgement from the broker before timing out.
7
8
  # @param required_acks [Integer] The number of replicas that must acknowledge
8
9
  # a write.
9
- def initialize(cluster:, logger:, timeout: 10_000, required_acks: 1)
10
- @cluster = cluster
10
+ def initialize(broker_pool:, logger:, timeout: 10_000, required_acks: 1)
11
+ @broker_pool = broker_pool
11
12
  @logger = logger
12
13
  @required_acks = required_acks
13
14
  @timeout = timeout
14
- @buffer = {}
15
+ @buffered_messages = []
15
16
  end
16
17
 
17
18
  def write(value, key:, topic:, partition:)
18
- message = Protocol::Message.new(value: value, key: key)
19
-
20
- @buffer[topic] ||= {}
21
- @buffer[topic][partition] ||= []
22
- @buffer[topic][partition] << message
19
+ @buffered_messages << Message.new(value, key: key, topic: topic, partition: partition)
23
20
  end
24
21
 
25
22
  def flush
26
- @cluster.produce(
27
- required_acks: @required_acks,
28
- timeout: @timeout,
29
- messages_for_topics: @buffer
30
- )
23
+ messages_for_broker = {}
24
+
25
+ @buffered_messages.each do |message|
26
+ broker = @broker_pool.get_leader(message.topic, message.partition)
27
+
28
+ messages_for_broker[broker] ||= []
29
+ messages_for_broker[broker] << message
30
+ end
31
+
32
+ messages_for_broker.each do |broker, messages|
33
+ @logger.info "Sending #{messages.count} messages to broker #{broker}"
34
+
35
+ message_set = MessageSet.new(messages)
36
+
37
+ response = broker.produce(
38
+ messages_for_topics: message_set.to_h,
39
+ required_acks: @required_acks,
40
+ timeout: @timeout,
41
+ )
42
+
43
+ if response
44
+ response.topics.each do |topic_info|
45
+ topic_info.partitions.each do |partition_info|
46
+ Protocol.handle_error(partition_info.error_code)
47
+ end
48
+ end
49
+ end
50
+ end
31
51
 
32
- @buffer = {}
52
+ @buffered_messages.clear
33
53
  end
34
54
  end
35
55
  end
@@ -2,6 +2,21 @@ module Kafka
2
2
  module Protocol
3
3
  PRODUCE_API_KEY = 0
4
4
  TOPIC_METADATA_API_KEY = 3
5
+
6
+ def self.handle_error(error_code)
7
+ case error_code
8
+ when -1 then raise UnknownError
9
+ when 0 then nil # no error, yay!
10
+ when 1 then raise OffsetOutOfRange
11
+ when 2 then raise CorruptMessage
12
+ when 3 then raise UnknownTopicOrPartition
13
+ when 4 then raise InvalidMessageSize
14
+ when 5 then raise LeaderNotAvailable
15
+ when 6 then raise NotLeaderForPartition
16
+ when 7 then raise RequestTimedOut
17
+ else raise UnknownError, "Unknown error with code #{error_code}"
18
+ end
19
+ end
5
20
  end
6
21
  end
7
22
 
@@ -1,31 +1,62 @@
1
1
  module Kafka
2
2
  module Protocol
3
+
4
+ # A decoder wraps an IO object, making it easy to read specific data types
5
+ # from it. The Kafka protocol is not self-describing, so a client must call
6
+ # these methods in just the right order for things to work.
3
7
  class Decoder
8
+
9
+ # Initializes a new decoder.
10
+ #
11
+ # @param io [IO] an object that acts as an IO.
4
12
  def initialize(io)
5
13
  @io = io
6
14
  end
7
15
 
16
+ # Decodes an 8-bit integer from the IO object.
17
+ #
18
+ # @return [Integer]
8
19
  def int8
9
20
  read(1).unpack("C").first
10
21
  end
11
22
 
23
+ # Decodes a 16-bit integer from the IO object.
24
+ #
25
+ # @return [Integer]
12
26
  def int16
13
27
  read(2).unpack("s>").first
14
28
  end
15
29
 
30
+ # Decodes a 32-bit integer from the IO object.
31
+ #
32
+ # @return [Integer]
16
33
  def int32
17
34
  read(4).unpack("l>").first
18
35
  end
19
36
 
37
+ # Decodes a 64-bit integer from the IO object.
38
+ #
39
+ # @return [Integer]
20
40
  def int64
21
41
  read(8).unpack("q>").first
22
42
  end
23
43
 
24
- def array
44
+ # Decodes an array from the IO object.
45
+ #
46
+ # The provided block will be called once for each item in the array. It is
47
+ # the responsibility of the block to decode the proper type in the block,
48
+ # since there's no information that allows the type to be inferred
49
+ # automatically.
50
+ #
51
+ # @return [Array]
52
+ def array(&block)
25
53
  size = int32
26
- size.times.map { yield }
54
+ size.times.map(&block)
27
55
  end
28
56
 
57
+ # Decodes a string from the IO object.
58
+ #
59
+ # @return [String]
29
60
  def string
30
61
  size = int16
31
62
 
@@ -36,6 +67,9 @@ module Kafka
36
67
  end
37
68
  end
38
69
 
70
+ # Decodes a list of bytes from the IO object.
71
+ #
72
+ # @return [String]
39
73
  def bytes
40
74
  size = int32
41
75
 
@@ -46,6 +80,10 @@ module Kafka
46
80
  end
47
81
  end
48
82
 
83
+ # Reads the specified number of bytes from the IO object, returning them
84
+ # as a String.
85
+ #
86
+ # @return [String]
49
87
  def read(number_of_bytes)
50
88
  @io.read(number_of_bytes)
51
89
  end
@@ -1,36 +1,77 @@
1
1
  module Kafka
2
2
  module Protocol
3
+
4
+ # An encoder wraps an IO object, making it easy to write specific data types
5
+ # to it.
3
6
  class Encoder
7
+
8
+ # Initializes a new encoder.
9
+ #
10
+ # @param io [IO] an object that acts as an IO.
4
11
  def initialize(io)
5
12
  @io = io
6
13
  @io.set_encoding(Encoding::BINARY)
7
14
  end
8
15
 
16
+ # Writes bytes directly to the IO object.
17
+ #
18
+ # @param bytes [String]
19
+ # @return [nil]
9
20
  def write(bytes)
10
21
  @io.write(bytes)
22
+
23
+ nil
11
24
  end
12
25
 
26
+ # Writes an 8-bit integer to the IO object.
27
+ #
28
+ # @param int [Integer]
29
+ # @return [nil]
13
30
  def write_int8(int)
14
31
  write([int].pack("C"))
15
32
  end
16
33
 
34
+ # Writes a 16-bit integer to the IO object.
35
+ #
36
+ # @param int [Integer]
37
+ # @return [nil]
17
38
  def write_int16(int)
18
39
  write([int].pack("s>"))
19
40
  end
20
41
 
42
+ # Writes a 32-bit integer to the IO object.
43
+ #
44
+ # @param int [Integer]
45
+ # @return [nil]
21
46
  def write_int32(int)
22
47
  write([int].pack("l>"))
23
48
  end
24
49
 
50
+ # Writes a 64-bit integer to the IO object.
51
+ #
52
+ # @param int [Integer]
53
+ # @return [nil]
25
54
  def write_int64(int)
26
55
  write([int].pack("q>"))
27
56
  end
28
57
 
58
+ # Writes an array to the IO object.
59
+ #
60
+ # Each item in the specified array will be yielded to the provided block;
61
+ # it's the responsibility of the block to write those items using the
62
+ # encoder.
63
+ #
64
+ # @param array [Array]
65
+ # @return [nil]
29
66
  def write_array(array, &block)
30
67
  write_int32(array.size)
31
68
  array.each(&block)
32
69
  end
33
70
 
71
+ # Writes a string to the IO object.
72
+ #
73
+ # @param string [String]
74
+ # @return [nil]
34
75
  def write_string(string)
35
76
  if string.nil?
36
77
  write_int16(-1)
@@ -40,6 +81,10 @@ module Kafka
40
81
  end
41
82
  end
42
83
 
84
+ # Writes a byte string to the IO object.
85
+ #
86
+ # @param bytes [String]
87
+ # @return [nil]
43
88
  def write_bytes(bytes)
44
89
  if bytes.nil?
45
90
  write_int32(-1)
@@ -48,6 +93,19 @@ module Kafka
48
93
  write(bytes)
49
94
  end
50
95
  end
96
+
97
+ # Encodes an object into a new buffer.
98
+ #
99
+ # @param object [#encode] the object that will encode itself.
100
+ # @return [String] the encoded data.
101
+ def self.encode_with(object)
102
+ buffer = StringIO.new
103
+ encoder = new(buffer)
104
+
105
+ object.encode(encoder)
106
+
107
+ buffer.string
108
+ end
51
109
  end
52
110
  end
53
111
  end
@@ -1,7 +1,37 @@
1
1
  module Kafka
2
2
  module Protocol
3
+
4
+ # A response to a {TopicMetadataRequest}.
5
+ #
6
+ # The response contains information on the brokers, topics, and partitions in
7
+ # the cluster.
8
+ #
9
+ # * For each broker a node id, host, and port is provided.
10
+ # * For each topic partition the node id of the broker acting as partition leader,
11
+ # as well as a list of node ids for the set of replicas, are given. The `isr` list is
12
+ # the subset of replicas that are "in sync", i.e. have fully caught up with the
13
+ # leader.
14
+ #
15
+ # == API Specification
16
+ #
17
+ # MetadataResponse => [Broker][TopicMetadata]
18
+ # Broker => NodeId Host Port (any number of brokers may be returned)
19
+ # NodeId => int32
20
+ # Host => string
21
+ # Port => int32
22
+ #
23
+ # TopicMetadata => TopicErrorCode TopicName [PartitionMetadata]
24
+ # TopicErrorCode => int16
25
+ #
26
+ # PartitionMetadata => PartitionErrorCode PartitionId Leader Replicas Isr
27
+ # PartitionErrorCode => int16
28
+ # PartitionId => int32
29
+ # Leader => int32
30
+ # Replicas => [int32]
31
+ # Isr => [int32]
32
+ #
3
33
  class MetadataResponse
4
- class Broker
34
+ class BrokerInfo
5
35
  attr_reader :node_id, :host, :port
6
36
 
7
37
  def initialize(node_id:, host:, port:)
@@ -9,9 +39,17 @@ module Kafka
9
39
  @host = host
10
40
  @port = port
11
41
  end
42
+
43
+ def inspect
44
+ "#{host}:#{port} (node_id=#{node_id})"
45
+ end
12
46
  end
13
47
 
14
48
  class PartitionMetadata
49
+ attr_reader :partition_id, :leader
50
+
51
+ attr_reader :partition_error_code
52
+
15
53
  def initialize(partition_error_code:, partition_id:, leader:, replicas:, isr:)
16
54
  @partition_error_code = partition_error_code
17
55
  @partition_id = partition_id
@@ -22,6 +60,14 @@ module Kafka
22
60
  end
23
61
 
24
62
  class TopicMetadata
63
+ # @return [String] the name of the topic
64
+ attr_reader :topic_name
65
+
66
+ # @return [Array<PartitionMetadata>] the partitions in the topic.
67
+ attr_reader :partitions
68
+
69
+ attr_reader :topic_error_code
70
+
25
71
  def initialize(topic_error_code:, topic_name:, partitions:)
26
72
  @topic_error_code = topic_error_code
27
73
  @topic_name = topic_name
@@ -29,22 +75,65 @@ module Kafka
29
75
  end
30
76
  end
31
77
 
32
- attr_reader :brokers, :topics
78
+ # @return [Array<BrokerInfo>] the list of brokers in the cluster.
79
+ attr_reader :brokers
80
+
81
+ # @return [Array<TopicMetadata>] the list of topics in the cluster.
82
+ attr_reader :topics
83
+
84
+ def initialize(brokers:, topics:)
85
+ @brokers = brokers
86
+ @topics = topics
87
+ end
88
+
89
+ # Finds the node id of the broker that is acting as leader for the given topic
90
+ # and partition per this metadata.
91
+ #
92
+ # @param topic [String] the name of the topic.
93
+ # @param partition [Integer] the partition number.
94
+ # @return [Integer] the node id of the leader.
95
+ def find_leader_id(topic, partition)
96
+ topic_info = @topics.find {|t| t.topic_name == topic }
97
+
98
+ if topic_info.nil?
99
+ raise "no topic #{topic}"
100
+ end
101
+
102
+ partition_info = topic_info.partitions.find {|p| p.partition_id == partition }
103
+
104
+ if partition_info.nil?
105
+ raise "no partition #{partition} in topic #{topic}"
106
+ end
107
+
108
+ partition_info.leader
109
+ end
110
+
111
+ # Finds the broker info for the given node id.
112
+ #
113
+ # @param node_id [Integer] the node id of the broker.
114
+ # @return [BrokerInfo] information about the broker.
115
+ def find_broker(node_id)
116
+ @brokers.find {|broker| broker.node_id == node_id }
117
+ end
33
118
 
34
- def decode(decoder)
35
- @brokers = decoder.array do
119
+ # Decodes a MetadataResponse from a {Decoder} containing response data.
120
+ #
121
+ # @param decoder [Decoder]
122
+ # @return [MetadataResponse] the metadata response.
123
+ def self.decode(decoder)
124
+ brokers = decoder.array do
36
125
  node_id = decoder.int32
37
126
  host = decoder.string
38
127
  port = decoder.int32
39
128
 
40
- Broker.new(
129
+ BrokerInfo.new(
41
130
  node_id: node_id,
42
131
  host: host,
43
132
  port: port
44
133
  )
45
134
  end
46
135
 
47
- @topics = decoder.array do
136
+ topics = decoder.array do
48
137
  topic_error_code = decoder.int16
49
138
  topic_name = decoder.string
50
139
 
@@ -64,6 +153,8 @@ module Kafka
64
153
  partitions: partitions,
65
154
  )
66
155
  end
156
+
157
+ new(brokers: brokers, topics: topics)
67
158
  end
68
159
  end
69
160
  end
@@ -74,7 +74,7 @@ module Kafka
74
74
  # When encoding a message into a message set, the bytesize of the message must
75
75
  # precede the actual bytes. Therefore we need to encode the message into a
76
76
  # separate buffer first.
77
- encoded_message = encode_message(message)
77
+ encoded_message = Encoder.encode_with(message)
78
78
 
79
79
  encoder.write_int64(offset)
80
80
 
@@ -84,15 +84,6 @@ module Kafka
84
84
 
85
85
  buffer.string
86
86
  end
87
-
88
- def encode_message(message)
89
- buffer = StringIO.new
90
- encoder = Encoder.new(buffer)
91
-
92
- message.encode(encoder)
93
-
94
- buffer.string
95
- end
96
87
  end
97
88
  end
98
89
  end
@@ -4,7 +4,7 @@ module Kafka
4
4
  class TopicInfo
5
5
  attr_reader :topic, :partitions
6
6
 
7
- def initialize(topic, partitions)
7
+ def initialize(topic:, partitions:)
8
8
  @topic = topic
9
9
  @partitions = partitions
10
10
  end
@@ -13,7 +13,7 @@ module Kafka
13
13
  class PartitionInfo
14
14
  attr_reader :partition, :error_code, :offset
15
15
 
16
- def initialize(partition, error_code, offset)
16
+ def initialize(partition:, error_code:, offset:)
17
17
  @partition = partition
18
18
  @error_code = error_code
19
19
  @offset = offset
@@ -22,20 +22,26 @@ module Kafka
22
22
 
23
23
  attr_reader :topics
24
24
 
25
- def decode(decoder)
26
- @topics = decoder.array do
25
+ def initialize(topics: [])
26
+ @topics = topics
27
+ end
28
+
29
+ def self.decode(decoder)
30
+ topics = decoder.array do
27
31
  topic = decoder.string
28
32
 
29
33
  partitions = decoder.array do
30
- partition = decoder.int32
31
- error_code = decoder.int16
32
- offset = decoder.int64
33
-
34
- PartitionInfo.new(partition, error_code, offset)
34
+ PartitionInfo.new(
35
+ partition: decoder.int32,
36
+ error_code: decoder.int16,
37
+ offset: decoder.int64,
38
+ )
35
39
  end
36
40
 
37
- TopicInfo.new(topic, partitions)
41
+ TopicInfo.new(topic: topic, partitions: partitions)
38
42
  end
43
+
44
+ new(topics: topics)
39
45
  end
40
46
  end
41
47
  end
@@ -1,3 +1,3 @@
1
1
  module Kafka
2
- VERSION = "0.1.0-alpha"
2
+ VERSION = "0.1.0-alpha2"
3
3
  end
@@ -0,0 +1,3 @@
1
+ # Needed because the gem is registered as `ruby-kafka`.
2
+
3
+ require "kafka"
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ docker-compose run kafka1 /opt/kafka_2.10-0.8.2.0/bin/kafka-topics.sh --create --topic test-messages --replication-factor 3 --partitions 5 --zookeeper zk
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha
4
+ version: 0.1.0.pre.alpha2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-01-19 00:00:00.000000000 Z
11
+ date: 2016-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -69,10 +69,15 @@ files:
69
69
  - Rakefile
70
70
  - bin/console
71
71
  - bin/setup
72
+ - docker-compose.yml
72
73
  - kafka.gemspec
73
74
  - lib/kafka.rb
74
- - lib/kafka/cluster.rb
75
+ - lib/kafka/broker.rb
76
+ - lib/kafka/broker_pool.rb
77
+ - lib/kafka/client.rb
75
78
  - lib/kafka/connection.rb
79
+ - lib/kafka/message.rb
80
+ - lib/kafka/message_set.rb
76
81
  - lib/kafka/producer.rb
77
82
  - lib/kafka/protocol.rb
78
83
  - lib/kafka/protocol/decoder.rb
@@ -84,6 +89,8 @@ files:
84
89
  - lib/kafka/protocol/request_message.rb
85
90
  - lib/kafka/protocol/topic_metadata_request.rb
86
91
  - lib/kafka/version.rb
92
+ - lib/ruby-kafka.rb
93
+ - test-setup.sh
87
94
  homepage: https://github.com/zendesk/ruby-kafka
88
95
  licenses:
89
96
  - Apache License Version 2.0
@@ -1,53 +0,0 @@
1
- require "logger"
2
- require "kafka/connection"
3
- require "kafka/protocol"
4
-
5
- module Kafka
6
- class Cluster
7
- def self.connect(brokers:, client_id:, logger:)
8
- host, port = brokers.first.split(":", 2)
9
-
10
- connection = Connection.new(
11
- host: host,
12
- port: port.to_i,
13
- client_id: client_id,
14
- logger: logger
15
- )
16
-
17
- connection.open
18
-
19
- new(connection: connection, logger: logger)
20
- end
21
-
22
- def initialize(connection:, logger: nil)
23
- @connection = connection
24
- @logger = logger
25
- end
26
-
27
- def fetch_metadata(**options)
28
- api_key = Protocol::TOPIC_METADATA_API_KEY
29
- request = Protocol::TopicMetadataRequest.new(**options)
30
- response = Protocol::MetadataResponse.new
31
-
32
- @connection.write_request(api_key, request)
33
- @connection.read_response(response)
34
-
35
- response
36
- end
37
-
38
- def produce(**options)
39
- api_key = Protocol::PRODUCE_API_KEY
40
- request = Protocol::ProduceRequest.new(**options)
41
-
42
- @connection.write_request(api_key, request)
43
-
44
- if request.requires_acks?
45
- response = Protocol::ProduceResponse.new
46
- @connection.read_response(response)
47
- response
48
- else
49
- nil
50
- end
51
- end
52
- end
53
- end