rdkafka 0.6.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +23 -0
  3. data/CHANGELOG.md +27 -0
  4. data/README.md +9 -9
  5. data/docker-compose.yml +17 -11
  6. data/ext/README.md +10 -15
  7. data/ext/Rakefile +24 -3
  8. data/lib/rdkafka.rb +8 -0
  9. data/lib/rdkafka/abstract_handle.rb +82 -0
  10. data/lib/rdkafka/admin.rb +155 -0
  11. data/lib/rdkafka/admin/create_topic_handle.rb +27 -0
  12. data/lib/rdkafka/admin/create_topic_report.rb +22 -0
  13. data/lib/rdkafka/admin/delete_topic_handle.rb +27 -0
  14. data/lib/rdkafka/admin/delete_topic_report.rb +22 -0
  15. data/lib/rdkafka/bindings.rb +64 -18
  16. data/lib/rdkafka/callbacks.rb +106 -0
  17. data/lib/rdkafka/config.rb +38 -9
  18. data/lib/rdkafka/consumer.rb +221 -46
  19. data/lib/rdkafka/consumer/headers.rb +7 -5
  20. data/lib/rdkafka/consumer/partition.rb +1 -1
  21. data/lib/rdkafka/consumer/topic_partition_list.rb +6 -16
  22. data/lib/rdkafka/error.rb +35 -4
  23. data/lib/rdkafka/metadata.rb +92 -0
  24. data/lib/rdkafka/producer.rb +50 -24
  25. data/lib/rdkafka/producer/delivery_handle.rb +7 -49
  26. data/lib/rdkafka/producer/delivery_report.rb +7 -2
  27. data/lib/rdkafka/version.rb +3 -3
  28. data/rdkafka.gemspec +3 -3
  29. data/spec/rdkafka/abstract_handle_spec.rb +114 -0
  30. data/spec/rdkafka/admin/create_topic_handle_spec.rb +52 -0
  31. data/spec/rdkafka/admin/create_topic_report_spec.rb +16 -0
  32. data/spec/rdkafka/admin/delete_topic_handle_spec.rb +52 -0
  33. data/spec/rdkafka/admin/delete_topic_report_spec.rb +16 -0
  34. data/spec/rdkafka/admin_spec.rb +203 -0
  35. data/spec/rdkafka/bindings_spec.rb +28 -10
  36. data/spec/rdkafka/callbacks_spec.rb +20 -0
  37. data/spec/rdkafka/config_spec.rb +51 -9
  38. data/spec/rdkafka/consumer/message_spec.rb +6 -1
  39. data/spec/rdkafka/consumer_spec.rb +287 -20
  40. data/spec/rdkafka/error_spec.rb +7 -3
  41. data/spec/rdkafka/metadata_spec.rb +78 -0
  42. data/spec/rdkafka/producer/delivery_handle_spec.rb +3 -43
  43. data/spec/rdkafka/producer/delivery_report_spec.rb +5 -1
  44. data/spec/rdkafka/producer_spec.rb +220 -100
  45. data/spec/spec_helper.rb +34 -6
  46. metadata +37 -13
  47. data/.travis.yml +0 -34
@@ -19,7 +19,7 @@ module Rdkafka
19
19
  raise Rdkafka::RdkafkaError.new(err, "Error reading message headers")
20
20
  end
21
21
 
22
- headers_ptr = headers_ptrptr.read(:pointer).tap { |it| it.autorelease = false }
22
+ headers_ptr = headers_ptrptr.read_pointer
23
23
 
24
24
  name_ptrptr = FFI::MemoryPointer.new(:pointer)
25
25
  value_ptrptr = FFI::MemoryPointer.new(:pointer)
@@ -42,12 +42,14 @@ module Rdkafka
42
42
  raise Rdkafka::RdkafkaError.new(err, "Error reading a message header at index #{idx}")
43
43
  end
44
44
 
45
- name = name_ptrptr.read(:pointer).tap { |it| it.autorelease = false }
46
- name = name.read_string_to_null
45
+ name_ptr = name_ptrptr.read_pointer
46
+ name = name_ptr.respond_to?(:read_string_to_null) ? name_ptr.read_string_to_null : name_ptr.read_string
47
47
 
48
48
  size = size_ptr[:value]
49
- value = value_ptrptr.read(:pointer).tap { |it| it.autorelease = false }
50
- value = value.read_string(size)
49
+
50
+ value_ptr = value_ptrptr.read_pointer
51
+
52
+ value = value_ptr.read_string(size)
51
53
 
52
54
  headers[name.to_sym] = value
53
55
 
@@ -11,7 +11,7 @@ module Rdkafka
11
11
  attr_reader :offset
12
12
 
13
13
  # Partition's error code
14
- # @retuen [Integer]
14
+ # @return [Integer]
15
15
  attr_reader :err
16
16
 
17
17
  # @private
@@ -4,7 +4,7 @@ module Rdkafka
4
4
  class TopicPartitionList
5
5
  # Create a topic partition list.
6
6
  #
7
- # @param data [Hash<String => [nil,Partition]>] The topic and partion data or nil to create an empty list
7
+ # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an empty list
8
8
  #
9
9
  # @return [TopicPartitionList]
10
10
  def initialize(data=nil)
@@ -71,7 +71,7 @@ module Rdkafka
71
71
 
72
72
  # Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
73
73
  #
74
- # @return [Hash<String, [Array<Partition>, nil]>]
74
+ # @return [Hash{String => Array<Partition>,nil}]
75
75
  def to_h
76
76
  @data
77
77
  end
@@ -106,7 +106,7 @@ module Rdkafka
106
106
  data[elem[:topic]] = nil
107
107
  else
108
108
  partitions = data[elem[:topic]] || []
109
- offset = if elem[:offset] == -1001
109
+ offset = if elem[:offset] == Rdkafka::Bindings::RD_KAFKA_OFFSET_INVALID
110
110
  nil
111
111
  else
112
112
  elem[:offset]
@@ -125,10 +125,10 @@ module Rdkafka
125
125
  #
126
126
  # The pointer will be cleaned by `rd_kafka_topic_partition_list_destroy` when GC releases it.
127
127
  #
128
- # @return [FFI::AutoPointer]
128
+ # @return [FFI::Pointer]
129
129
  # @private
130
130
  def to_native_tpl
131
- tpl = TopicPartitionList.new_native_tpl(count)
131
+ tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
132
132
 
133
133
  @data.each do |topic, partitions|
134
134
  if partitions
@@ -138,6 +138,7 @@ module Rdkafka
138
138
  topic,
139
139
  p.partition
140
140
  )
141
+
141
142
  if p.offset
142
143
  Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
143
144
  tpl,
@@ -158,17 +159,6 @@ module Rdkafka
158
159
 
159
160
  tpl
160
161
  end
161
-
162
- # Creates a new native tpl and wraps it into FFI::AutoPointer which in turn calls
163
- # `rd_kafka_topic_partition_list_destroy` when a pointer will be cleaned by GC
164
- #
165
- # @param count [Integer] an initial capacity of partitions list
166
- # @return [FFI::AutoPointer]
167
- # @private
168
- def self.new_native_tpl(count)
169
- tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
170
- FFI::AutoPointer.new(tpl, Rdkafka::Bindings.method(:rd_kafka_topic_partition_list_destroy))
171
- end
172
162
  end
173
163
  end
174
164
  end
data/lib/rdkafka/error.rb CHANGED
@@ -1,15 +1,27 @@
1
1
  module Rdkafka
2
+ # Base error class.
3
+ class BaseError < RuntimeError; end
4
+
2
5
  # Error returned by the underlying rdkafka library.
3
- class RdkafkaError < RuntimeError
6
+ class RdkafkaError < BaseError
4
7
  # The underlying raw error response
5
8
  # @return [Integer]
6
- attr_reader :rdkafka_response, :message_prefix
9
+ attr_reader :rdkafka_response
10
+
11
+ # Prefix to be used for human readable representation
12
+ # @return [String]
13
+ attr_reader :message_prefix
14
+
15
+ # Error message sent by the broker
16
+ # @return [String]
17
+ attr_reader :broker_message
7
18
 
8
19
  # @private
9
- def initialize(response, message_prefix=nil)
20
+ def initialize(response, message_prefix=nil, broker_message: nil)
10
21
  raise TypeError.new("Response has to be an integer") unless response.is_a? Integer
11
22
  @rdkafka_response = response
12
23
  @message_prefix = message_prefix
24
+ @broker_message = broker_message
13
25
  end
14
26
 
15
27
  # This error's code, for example `:partition_eof`, `:msg_size_too_large`.
@@ -39,9 +51,14 @@ module Rdkafka
39
51
  def is_partition_eof?
40
52
  code == :partition_eof
41
53
  end
54
+
55
+ # Error comparison
56
+ def ==(another_error)
57
+ another_error.is_a?(self.class) && (self.to_s == another_error.to_s)
58
+ end
42
59
  end
43
60
 
44
- # Error with potic partition list returned by the underlying rdkafka library.
61
+ # Error with topic partition list returned by the underlying rdkafka library.
45
62
  class RdkafkaTopicPartitionListError < RdkafkaError
46
63
  # @return [TopicPartitionList]
47
64
  attr_reader :topic_partition_list
@@ -52,4 +69,18 @@ module Rdkafka
52
69
  @topic_partition_list = topic_partition_list
53
70
  end
54
71
  end
72
+
73
+ # Error class for public consumer method calls on a closed consumer.
74
+ class ClosedConsumerError < BaseError
75
+ def initialize(method)
76
+ super("Illegal call to #{method.to_s} on a closed consumer")
77
+ end
78
+ end
79
+
80
+ # Error class for public producer method calls on a closed producer.
81
+ class ClosedProducerError < BaseError
82
+ def initialize(method)
83
+ super("Illegal call to #{method.to_s} on a closed producer")
84
+ end
85
+ end
55
86
  end
@@ -0,0 +1,92 @@
1
+ module Rdkafka
2
+ class Metadata
3
+ attr_reader :brokers, :topics
4
+
5
+ def initialize(native_client, topic_name = nil)
6
+ native_topic = if topic_name
7
+ Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil)
8
+ end
9
+
10
+ ptr = FFI::MemoryPointer.new(:pointer)
11
+
12
+ # If topic_flag is 1, we request info about *all* topics in the cluster. If topic_flag is 0,
13
+ # we only request info about locally known topics (or a single topic if one is passed in).
14
+ topic_flag = topic_name.nil? ? 1 : 0
15
+
16
+ # Retrieve the Metadata
17
+ result = Rdkafka::Bindings.rd_kafka_metadata(native_client, topic_flag, native_topic, ptr, 250)
18
+
19
+ # Error Handling
20
+ raise Rdkafka::RdkafkaError.new(result) unless result.zero?
21
+
22
+ metadata_from_native(ptr.read_pointer)
23
+ ensure
24
+ Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if topic_name
25
+ Rdkafka::Bindings.rd_kafka_metadata_destroy(ptr.read_pointer)
26
+ end
27
+
28
+ private
29
+
30
+ def metadata_from_native(ptr)
31
+ metadata = Metadata.new(ptr)
32
+ @brokers = Array.new(metadata[:brokers_count]) do |i|
33
+ BrokerMetadata.new(metadata[:brokers_metadata] + (i * BrokerMetadata.size)).to_h
34
+ end
35
+
36
+ @topics = Array.new(metadata[:topics_count]) do |i|
37
+ topic = TopicMetadata.new(metadata[:topics_metadata] + (i * TopicMetadata.size))
38
+ raise Rdkafka::RdkafkaError.new(topic[:rd_kafka_resp_err]) unless topic[:rd_kafka_resp_err].zero?
39
+
40
+ partitions = Array.new(topic[:partition_count]) do |j|
41
+ partition = PartitionMetadata.new(topic[:partitions_metadata] + (j * PartitionMetadata.size))
42
+ raise Rdkafka::RdkafkaError.new(partition[:rd_kafka_resp_err]) unless partition[:rd_kafka_resp_err].zero?
43
+ partition.to_h
44
+ end
45
+ topic.to_h.merge!(partitions: partitions)
46
+ end
47
+ end
48
+
49
+ class CustomFFIStruct < FFI::Struct
50
+ def to_h
51
+ members.each_with_object({}) do |mem, hsh|
52
+ val = self.[](mem)
53
+ next if val.is_a?(FFI::Pointer) || mem == :rd_kafka_resp_err
54
+
55
+ hsh[mem] = self.[](mem)
56
+ end
57
+ end
58
+ end
59
+
60
+ class Metadata < CustomFFIStruct
61
+ layout :brokers_count, :int,
62
+ :brokers_metadata, :pointer,
63
+ :topics_count, :int,
64
+ :topics_metadata, :pointer,
65
+ :broker_id, :int32,
66
+ :broker_name, :string
67
+ end
68
+
69
+ class BrokerMetadata < CustomFFIStruct
70
+ layout :broker_id, :int32,
71
+ :broker_name, :string,
72
+ :broker_port, :int
73
+ end
74
+
75
+ class TopicMetadata < CustomFFIStruct
76
+ layout :topic_name, :string,
77
+ :partition_count, :int,
78
+ :partitions_metadata, :pointer,
79
+ :rd_kafka_resp_err, :int
80
+ end
81
+
82
+ class PartitionMetadata < CustomFFIStruct
83
+ layout :partition_id, :int32,
84
+ :rd_kafka_resp_err, :int,
85
+ :leader, :int32,
86
+ :replica_count, :int,
87
+ :replicas, :pointer,
88
+ :in_sync_replica_brokers, :int,
89
+ :isrs, :pointer
90
+ end
91
+ end
92
+ end
@@ -2,12 +2,19 @@ module Rdkafka
2
2
  # A producer for Kafka messages. To create a producer set up a {Config} and call {Config#producer producer} on that.
3
3
  class Producer
4
4
  # @private
5
- @delivery_callback = nil
5
+ # Returns the current delivery callback, by default this is nil.
6
+ #
7
+ # @return [Proc, nil]
8
+ attr_reader :delivery_callback
6
9
 
7
10
  # @private
8
11
  def initialize(native_kafka)
9
12
  @closing = false
10
13
  @native_kafka = native_kafka
14
+
15
+ # Makes sure, that the producer gets closed before it gets GCed by Ruby
16
+ ObjectSpace.define_finalizer(self, proc { close })
17
+
11
18
  # Start thread to poll client for delivery callbacks
12
19
  @polling_thread = Thread.new do
13
20
  loop do
@@ -24,33 +31,42 @@ module Rdkafka
24
31
  # Set a callback that will be called every time a message is successfully produced.
25
32
  # The callback is called with a {DeliveryReport}
26
33
  #
27
- # @param callback [Proc] The callback
34
+ # @param callback [Proc, #call] The callback
28
35
  #
29
36
  # @return [nil]
30
37
  def delivery_callback=(callback)
31
- raise TypeError.new("Callback has to be a proc or lambda") unless callback.is_a? Proc
38
+ raise TypeError.new("Callback has to be callable") unless callback.respond_to?(:call)
32
39
  @delivery_callback = callback
33
40
  end
34
41
 
35
- # Returns the current delivery callback, by default this is nil.
36
- #
37
- # @return [Proc, nil]
38
- def delivery_callback
39
- @delivery_callback
40
- end
41
-
42
42
  # Close this producer and wait for the internal poll queue to empty.
43
43
  def close
44
+ return unless @native_kafka
45
+
44
46
  # Indicate to polling thread that we're closing
45
47
  @closing = true
46
48
  # Wait for the polling thread to finish up
47
49
  @polling_thread.join
50
+ Rdkafka::Bindings.rd_kafka_destroy(@native_kafka)
51
+ @native_kafka = nil
52
+ end
53
+
54
+ # Partition count for a given topic.
55
+ # NOTE: If 'allow.auto.create.topics' is set to true in the broker, the topic will be auto-created after returning nil.
56
+ #
57
+ # @param topic [String] The topic name.
58
+ #
59
+ # @return partition count [Integer,nil]
60
+ #
61
+ def partition_count(topic)
62
+ closed_producer_check(__method__)
63
+ Rdkafka::Metadata.new(@native_kafka, topic).topics&.first[:partition_count]
48
64
  end
49
65
 
50
66
  # Produces a message to a Kafka topic. The message is added to rdkafka's queue, call {DeliveryHandle#wait wait} on the returned delivery handle to make sure it is delivered.
51
67
  #
52
68
  # When no partition is specified the underlying Kafka library picks a partition based on the key. If no key is specified, a random partition will be used.
53
- # When a timestamp is provided this is used instead of the autogenerated timestamp.
69
+ # When a timestamp is provided this is used instead of the auto-generated timestamp.
54
70
  #
55
71
  # @param topic [String] The topic to produce to
56
72
  # @param payload [String,nil] The message's payload
@@ -62,7 +78,9 @@ module Rdkafka
62
78
  # @raise [RdkafkaError] When adding the message to rdkafka's queue failed
63
79
  #
64
80
  # @return [DeliveryHandle] Delivery handle that can be used to wait for the result of producing this message
65
- def produce(topic:, payload: nil, key: nil, partition: nil, timestamp: nil, headers: nil)
81
+ def produce(topic:, payload: nil, key: nil, partition: nil, partition_key: nil, timestamp: nil, headers: nil)
82
+ closed_producer_check(__method__)
83
+
66
84
  # Start by checking and converting the input
67
85
 
68
86
  # Get payload length
@@ -79,9 +97,15 @@ module Rdkafka
79
97
  key.bytesize
80
98
  end
81
99
 
82
- # If partition is nil use -1 to let Kafka set the partition based
83
- # on the key/randomly if there is no key
84
- partition = -1 if partition.nil?
100
+ if partition_key
101
+ partition_count = partition_count(topic)
102
+ # If the topic is not present, set to -1
103
+ partition = Rdkafka::Bindings.partitioner(partition_key, partition_count) if partition_count
104
+ end
105
+
106
+ # If partition is nil, use -1 to let librdafka set the partition randomly or
107
+ # based on the key when present.
108
+ partition ||= -1
85
109
 
86
110
  # If timestamp is nil use 0 and let Kafka set one. If an integer or time
87
111
  # use it.
@@ -100,7 +124,7 @@ module Rdkafka
100
124
  delivery_handle[:response] = -1
101
125
  delivery_handle[:partition] = -1
102
126
  delivery_handle[:offset] = -1
103
- DeliveryHandle.register(delivery_handle.to_ptr.address, delivery_handle)
127
+ DeliveryHandle.register(delivery_handle)
104
128
 
105
129
  args = [
106
130
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TOPIC, :string, topic,
@@ -116,16 +140,14 @@ module Rdkafka
116
140
  headers.each do |key0, value0|
117
141
  key = key0.to_s
118
142
  value = value0.to_s
119
- args += [
120
- :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER,
121
- :string, key,
122
- :pointer, value,
123
- :size_t, value.bytes.size
124
- ]
143
+ args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_HEADER
144
+ args << :string << key
145
+ args << :pointer << value
146
+ args << :size_t << value.bytes.size
125
147
  end
126
148
  end
127
149
 
128
- args += [:int, Rdkafka::Bindings::RD_KAFKA_VTYPE_END]
150
+ args << :int << Rdkafka::Bindings::RD_KAFKA_VTYPE_END
129
151
 
130
152
  # Produce the message
131
153
  response = Rdkafka::Bindings.rd_kafka_producev(
@@ -133,7 +155,7 @@ module Rdkafka
133
155
  *args
134
156
  )
135
157
 
136
- # Raise error if the produce call was not successfull
158
+ # Raise error if the produce call was not successful
137
159
  if response != 0
138
160
  DeliveryHandle.remove(delivery_handle.to_ptr.address)
139
161
  raise RdkafkaError.new(response)
@@ -146,5 +168,9 @@ module Rdkafka
146
168
  def call_delivery_callback(delivery_handle)
147
169
  @delivery_callback.call(delivery_handle) if @delivery_callback
148
170
  end
171
+
172
+ def closed_producer_check(method)
173
+ raise Rdkafka::ClosedProducerError.new(method) if @native_kafka.nil?
174
+ end
149
175
  end
150
176
  end
@@ -2,63 +2,21 @@ module Rdkafka
2
2
  class Producer
3
3
  # Handle to wait for a delivery report which is returned when
4
4
  # producing a message.
5
- class DeliveryHandle < FFI::Struct
5
+ class DeliveryHandle < Rdkafka::AbstractHandle
6
6
  layout :pending, :bool,
7
7
  :response, :int,
8
8
  :partition, :int,
9
9
  :offset, :int64
10
10
 
11
- REGISTRY = {}
12
-
13
- def self.register(address, handle)
14
- REGISTRY[address] = handle
15
- end
16
-
17
- def self.remove(address)
18
- REGISTRY.delete(address)
11
+ # @return [String] the name of the operation (e.g. "delivery")
12
+ def operation_name
13
+ "delivery"
19
14
  end
20
15
 
21
- # Whether the delivery handle is still pending.
22
- #
23
- # @return [Boolean]
24
- def pending?
25
- self[:pending]
16
+ # @return [DeliveryReport] a report on the delivery of the message
17
+ def create_result
18
+ DeliveryReport.new(self[:partition], self[:offset])
26
19
  end
27
-
28
- # Wait for the delivery report or raise an error if this takes longer than the timeout.
29
- # If there is a timeout this does not mean the message is not delivered, rdkafka might still be working on delivering the message.
30
- # In this case it is possible to call wait again.
31
- #
32
- # @param timeout_in_seconds [Integer, nil] Number of seconds to wait before timing out. If this is nil it does not time out.
33
- #
34
- # @raise [RdkafkaError] When delivering the message failed
35
- # @raise [WaitTimeoutError] When the timeout has been reached and the handle is still pending
36
- #
37
- # @return [DeliveryReport]
38
- def wait(timeout_in_seconds=60)
39
- timeout = if timeout_in_seconds
40
- Time.now.to_i + timeout_in_seconds
41
- else
42
- nil
43
- end
44
- loop do
45
- if pending?
46
- if timeout && timeout <= Time.now.to_i
47
- raise WaitTimeoutError.new("Waiting for delivery timed out after #{timeout_in_seconds} seconds")
48
- end
49
- sleep 0.1
50
- next
51
- elsif self[:response] != 0
52
- raise RdkafkaError.new(self[:response])
53
- else
54
- return DeliveryReport.new(self[:partition], self[:offset])
55
- end
56
- end
57
- end
58
-
59
- # Error that is raised when waiting for a delivery handle to complete
60
- # takes longer than the specified timeout.
61
- class WaitTimeoutError < RuntimeError; end
62
20
  end
63
21
  end
64
22
  end